Initial commit to github

This commit is contained in:
Keith Solomon
2025-08-22 15:40:01 -05:00
commit e8efdbeb34
230 changed files with 32213 additions and 0 deletions

7
static/img/logo.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg width="83px" height="83px" viewBox="0 0 83 83" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Copyright (c) 2025 Kevinleary.net, LLC. All rights reserved. -->
<title>kevinleary.net</title>
<circle fill="#FFFFFF" cx="41.5" cy="41.5" r="41.5"></circle>
<path d="M41.4990862,5 C21.3735072,5 5,21.3753349 5,41.5009138 C5,61.6264928 21.3735072,78 41.5009138,78 C61.6264928,78 78,61.6264928 78,41.5009138 C78,21.3753349 61.6264928,5 41.4990862,5 Z M50.341036,59.1902962 L34.0314949,42.9703077 L33.9218386,43.0799639 L33.9218386,59.1902962 L30.0893523,59.1902962 L30.0893523,22.8904188 L33.9218386,22.8904188 L33.9218386,37.9517062 L49.2389906,22.8904188 L54.5006634,22.8904188 L36.7016248,40.400696 L55.5387427,59.1902962 L50.341036,59.1902962 Z" fill="#000000" fill-rule="nonzero"></path>
<path d="M41.5,0 C18.5798958,0 0,18.5798958 0,41.5 C0,64.418375 18.5798958,83 41.5,83 C64.418375,83 83,64.4201042 83,41.5 C83,18.5798958 64.4201042,0 41.5,0 Z M41.5,80.2934783 C20.1078009,80.2934783 2.70652174,62.8907964 2.70652174,41.5009041 C2.70652174,20.1092036 20.1096092,2.70652174 41.5,2.70652174 C62.8903908,2.70652174 80.2934783,20.1092036 80.2934783,41.5009041 C80.2934783,62.8907964 62.8903908,80.2934783 41.5,80.2934783 Z" fill="#000000" fill-rule="nonzero"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/img/select_arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

12
static/js/admin.js Normal file
View File

@@ -0,0 +1,12 @@
/**
* Admin JS
*/
import { registerButtonComponent } from './components/button.js';
const app = () => {
registerButtonComponent();
}
document.addEventListener('DOMContentLoaded', app);
console.log(`admin.js loaded.`);

View File

@@ -0,0 +1,23 @@
// Back to Top Button Component
class BackToTopButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<button id="backToTopBtn" aria-label="Back to top" class="back-to-top" style="display:none;position:fixed;bottom:2rem;right:2rem;z-index:1000;padding:0.75em 1.5em;font-size:1.1rem;border-radius:2em;background:var(--color-primary,#3857BC);color:#fff;border:none;box-shadow:0 2px 8px rgba(0,0,0,0.15);cursor:pointer;transition:opacity 0.2s;">
↑ Top
</button>
`;
const btn = this.querySelector('#backToTopBtn');
window.addEventListener('scroll', () => {
btn.style.display = window.scrollY > 300 ? 'block' : 'none';
});
btn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
}
export function registerBackToTopButton() {
if (!customElements.get('back-to-top')) {
customElements.define('back-to-top', BackToTopButton);
}
}

View File

@@ -0,0 +1,76 @@
class ButtonComponent extends HTMLElement {
/**
* Parameters
* - btnClasses: Additional classes to add to the block.
* - element: The element to use for the button. Defaults to 'a'.
* - url: The URL to link to.
* - target: The target for the link.
* - title: The text to display on the button.
* - color: The color of the button.
* - variant: The variant of the button.
* - size: The size of the button.
* - width: The width of the button.
*
*/
connectedCallback() {
if (!this.querySelector(this.getAttribute('element'))) {
this.append(document.createElement(this.getAttribute('element')));
}
this.update();
}
static get observedAttributes() {
return [
'btnClasses',
'el',
'element',
'type',
'url',
'target',
'title',
'color',
'variant',
'size',
'width',
]
}
attributeChangedCallback() {
this.update();
}
update() {
const btn = this.querySelector(this.getAttribute('element'));
if (btn) {
btn.classList = this.getAttribute('btnClasses') || '';
if (this.getAttribute('element') == 'a') {
btn.href = this.getAttribute('url') || '#';
if (btn.target) {
btn.target = 'target="${this.getAttribute(target)}"';
}
}
const type = this.getAttribute('type');
if (type && this.getAttribute('element') !== 'a') {
btn.type = type;
}
btn.title = this.getAttribute('title') || '';
btn.textContent = this.getAttribute('title') || '';
btn.setAttribute('data-button-color', this.getAttribute('color'));
btn.setAttribute('data-button-variant', this.getAttribute('variant'));
btn.setAttribute('data-button-size', this.getAttribute('size'));
btn.setAttribute('data-button-width', this.getAttribute('width'));
}
}
}
export const registerButtonComponent = () => {
customElements.define('x-button', ButtonComponent);
}

View File

@@ -0,0 +1,17 @@
/**
* Add Focus Styling Back
* -
* On the first "Tab" key press, remove the "zero/hidden" styling of the outline for :focus states.
*/
function AddFocusStylingBack() {
function addFocusStylingBack(event) {
if (event.key == "Tab") { document.body.classList.add('keyboard-navigation'); }
document.removeEventListener("keydown", addFocusStylingBack);
}
document.addEventListener("keydown", addFocusStylingBack);
}
export default AddFocusStylingBack;

View File

@@ -0,0 +1,12 @@
/**
* Get Header Height
* -
* Get header height and set CSS variable "--hgtHeader" to the header height.
*/
function getHeaderHeight() {
const headerHeight = document.querySelector('.header__nav-main').getBoundingClientRect().height;
document.documentElement.style.setProperty('--hgtHeader', `${headerHeight}px`);
}
export default getHeaderHeight;

View File

@@ -0,0 +1,225 @@
/**
* VDI Main Nav - All Main Navigation Functionality
*
* Please review documentation upon first use, and, as-needed:
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
*/
/**
* Navigation
* Handles navigation logic
*
* @param {string} mobileMenuButtonId
* @param {string} dropDownClass
* @param {string} subMenuLinkClass
*/
class Navigation {
/**
* The main toggle element
*
* @type {HTMLElement}
* @link {https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties}
*/
#mobileMenuButton;
/**
* List of sub dropdown buttons
*
* @type {NodeList}
* @link {https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties}
*/
#dropdownButtons;
/**
* Class name to identify sub menu items (ul>li>a)
* @type {string}
*/
#subMenuElementIdentifier;
constructor(mobileMenuButtonId, dropDownClass, subMenuLinkClass = "sub-menu-item") {
this.#mobileMenuButton = document.getElementById(mobileMenuButtonId);
this.#dropdownButtons = document.querySelectorAll(dropDownClass); // Do not change this to getElementsByClassName, logic is iterating over non-live node list
this.#subMenuElementIdentifier = subMenuLinkClass;
this.handleEscapeKey();
}
/**
* Handles main mobile toggling
* Adds an event listener to mobile menu dropdown
* toggle button
*/
mobileMenuToggle() {
this.#mobileMenuButton.addEventListener("click", (event) => {
this.toggleMobileMenu(event, this.#mobileMenuButton); // toggle submenu on mobile
});
this.closeOnBlur(this.#mobileMenuButton, { "navType": "desktop" }); // close submenu when user clicks outside
this.closeOnBlur(this.#mobileMenuButton, { "navType": "mobile" });
}
/**
* Handles dropdowns on desktop navigation
* Loops over list of navigation dropdown buttons
* and adds eventlisteners to toggle view
*/
desktopMenuDropdowns() {
this.#dropdownButtons.forEach((button) => {
button.addEventListener("click", (event) => {
this.toggleDropdown(event, button);
});
this.closeOnBlur(button); // close menu when user clicks outside
this.handleFocus(button); // close dropdown when user finishes tabbing submenu elements
});
}
/**
* Toggles desktop dropdowns
*
* @param {EventTarget} event
* @param {HTMLButtonElement} button
*/
toggleDropdown(event, button) {
this.toggleAriaExpanded(event, button);
}
/**
* Toggles mobile menu
*
* @param {EventTarget} event
* @param {HTMLButtonElement} button
*/
toggleMobileMenu(event, button) {
this.toggleAriaExpanded(event, button)
}
/**
* Toggles aria-expanded attribute
*
* @param {EventTarget} event
* @param {HTMLButtonElement} button
*/
toggleAriaExpanded(event, button) {
let isExpanded = event.currentTarget.getAttribute("aria-expanded") === "true"; // true is returned as string
// ... but open the targeted secondary nav
if (!isExpanded) {
// close all dropdowns...
this.closeAllDropDowns();
// then toggle targeted dropdown...
button.setAttribute("aria-expanded", true);
} else {
button.setAttribute("aria-expanded", false);
}
}
/**
* Close dropdown when user clicks anywhere else on the screen
*
* @param {HTMLButtonElement} button
* @link {https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event}
* @link {https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/relatedTarget}
*/
closeOnBlur(button, args) {
if (args === undefined || args?.navType === "desktop") {
button.addEventListener("blur", (event) => {
if (event.relatedTarget == null) {
event.currentTarget.setAttribute("aria-expanded", false);
}
}, { passive: true });
}
//FIXME: Remove hardcoded literal
if (args?.navType === "mobile") {
document.getElementById("menu-container").addEventListener("blur", (event) => {
// Note: cannot group negation -1*-1*-1 != -(1*1*1)
const isNull = event.relatedTarget == null;
const isNotMenuItem = !isNull && !event.relatedTarget.classList.contains("menu-vdi__toggle")
&& !event.relatedTarget.classList.contains("sub-menu-item") && !event.relatedTarget.classList.contains("menu-vdi__link");
if (isNull || isNotMenuItem)
button.setAttribute("aria-expanded", false);
}, true)
}
}
/**
* Handles escape behaviour
* Closes all dropdown when user hits
* escape key
*
* @link {https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event}
*/
handleEscapeKey() {
window.addEventListener("keyup", (event) => {
if (event.key === "Escape") {
this.#mobileMenuButton.setAttribute("aria-expanded", false);
this.closeAllDropDowns();
}
}, { passive: true });
}
/**
* Close all dropdown menus
* Sets aria expanded property by looping
* over the list of dropdown button elements
*
* @link {https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded}
*/
closeAllDropDowns() {
this.#dropdownButtons.forEach((button) => {
button.setAttribute("aria-expanded", false);
});
}
/**
* Handle focus
* Watches blur event on submenu list container, if the next
* focus element is not a submenu list item, closes the
* dropdown.
*
* Implemented for WCAG 2.2 compliance
*
* @param {htmlButtonElement} button
*/
handleFocus(button) {
const subMenuListElement = button.closest("ul");
/**
* Ducking JavaScript, I am not sure why the blur event on submenu(ul)
* would bubble up to the button since the button and sub menu list element
* are siblings and not nested. This is a mystery to me.
*
* Here we are stopping any bubbling event that may be propagated to the
* top level buttons. This includes the bubbling event from sub menu list element.
*
* If anyone finds a better solution to this or can explain bubbling of focus event
* to next sibling, you time and effort would be much appreciated.
*/
button.addEventListener("focusout", (event) => {
event.stopImmediatePropagation();
})
subMenuListElement.addEventListener("focusout", (event) => {
// blur event triggers when user clicks outside
const nextFocusElement = event.relatedTarget;
let isSubMenuElement;
if (nextFocusElement !== null) {
isSubMenuElement = nextFocusElement.classList.contains(this.#subMenuElementIdentifier);
} else {
isSubMenuElement = false; // close when user clicks outside
}
if (!isSubMenuElement) {
this.closeAllDropDowns();
}
});
}
}
export default Navigation;

74
static/js/theme.js Normal file
View File

@@ -0,0 +1,74 @@
/**
* Theme JS
*/
import { registerButtonComponent } from './components/button.js';
import { registerBackToTopButton } from './components/backToTop.js';
import AddFocusStylingBack from './modules/FocusStyling.js';
import GetHeaderHeight from './modules/GetHeaderHeight.js';
import Navigation from './modules/Navigation.js';
// Add passive event listeners
! function (e) {
"function" == typeof define && define.amd ? define(e) : e()
}(function () {
let e;
const t = ["scroll", "wheel", "touchstart", "touchmove", "touchenter", "touchend", "touchleave", "mouseout", "mouseleave", "mouseup", "mousedown", "mousemove", "mouseenter", "mousewheel", "mouseover"];
if (function () {
let e = !1;
try {
const t = Object.defineProperty({}, "passive", {
get: function () {
e = !0
}
});
window.addEventListener("test", null, t);
window.removeEventListener("test", null, t);
} catch (e) { }
return e
}()) {
const n = EventTarget.prototype.addEventListener;
e = n;
EventTarget.prototype.addEventListener = function (n, o, r) {
let i;
const s = "object" == typeof r && null !== r,
u = s ? r.capture : r;
if (s) {
const t = Object.getOwnPropertyDescriptor(r, "passive");
r = t && !0 !== t.writable && void 0 === t.set ? { ...r } : r;
} else {
r = {};
}
r.passive = void 0 !== (i = r.passive) ? i : -1 !== t.indexOf(n) && !0;
r.capture = void 0 !== u && u;
e.call(this, n, o, r);
};
EventTarget.prototype.addEventListener._original = e
}
});
/**
* Application entrypoint
*/
document.addEventListener('DOMContentLoaded', () => {
// Register button component
registerButtonComponent();
registerBackToTopButton();
// Initialize Navigation
const navigation = new Navigation('navMainToggle', '.menu-vdi__toggle');
// Initialize Navigation
navigation.desktopMenuDropdowns();
navigation.mobileMenuToggle();
// Initialize Focus Styling and Header Height
AddFocusStylingBack();
GetHeaderHeight();
// Add Back to Top button to body
const backToTop = document.createElement('back-to-top');
document.body.appendChild(backToTop);
});
console.log(`theme.js loaded.`);