build fixes and static files fix, closes #28

This commit is contained in:
badblocks 2025-04-19 17:10:46 -07:00
parent bff2525c65
commit 6a44ef30a3
26 changed files with 91 additions and 39 deletions

48
static/css/base.css Normal file
View file

@ -0,0 +1,48 @@
[x-cloak] { display: none !important; }
/* Beta Badge */
#navbar-logo::after {
content: 'BETA';
font-size: 12px;
font-weight: bold;
color: var(--color-base-content);
background-color: var(--color-base-300);
padding: 4px 8px;
border-radius: 4px;
}
.gravatar-hovercard .gravatar-hovercard__inner {
background-color: var(--color-base-100) !important;
border-color: var(--color-base-300) !important;
color: var(--color-base-content) !important;
}
.gravatar-hovercard .gravatar-hovercard__inner,
.gravatar-hovercard .gravatar-hovercard__header-image,
.gravatar-hovercard .gravatar-hovercard__header,
.gravatar-hovercard .gravatar-hovercard__avatar-link,
.gravatar-hovercard .gravatar-hovercard__avatar,
.gravatar-hovercard .gravatar-hovercard__personal-info-plink,
.gravatar-hovercard .gravatar-hovercard__name,
.gravatar-hovercard .gravatar-hovercard__job,
.gravatar-hovercard .gravatar-hovercard__location,
.gravatar-hovercard .gravatar-hovercard__body,
.gravatar-hovercard .gravatar-hovercard__description,
.gravatar-hovercard .gravatar-hovercard__social-links,
.gravatar-hovercard .gravatar-hovercard__buttons,
.gravatar-hovercard .gravatar-hovercard__button,
.gravatar-hovercard .gravatar-hovercard__button:hover,
.gravatar-hovercard .gravatar-hovercard__footer,
.gravatar-hovercard .gravatar-hovercard__profile-url,
.gravatar-hovercard .gravatar-hovercard__profile-link,
.gravatar-hovercard .gravatar-hovercard__profile-color {
color: var(--color-base-content) !important;
}
.gravatar-hovercard .gravatar-hovercard__location {
color: var(--color-base-content) !important;
}
.dark .gravatar-hovercard .gravatar-hovercard__social-icon {
filter: invert(1) !important;
}

View file

@ -0,0 +1,88 @@
select.card-multiselect {
height: calc(var(--spacing) * 35);
/*background-image: linear-gradient(45deg, #0000 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, #0000 50%); */
background-image: none;
}
.choices.is-disabled .choices__inner,
.choices.is-disabled .choices__input {
background-color: var(--color-neutral);
}
.choices[data-type*=select-one] .choices__input {
border-bottom: 1px solid var(--btn-shadow);
background-color: var(--color-base-100);
}
.choices[data-type*=select-one] .choices__button:focus {
box-shadow: 0 0 0 2px #005F75;
}
.choices__inner {
background-color: var(--color-base-100);
border: 1px solid var(--btn-shadow);
}
.is-focused .choices__inner, .is-open .choices__inner {
border-color: var(--btn-shadow);
}
.choices__list--multiple .choices__item {
background-color: var(--color-base-100);
border: 1px solid var(--btn-shadow);
color: var(--color-primary);
}
.choices__list--multiple .choices__item.is-highlighted {
background-color: var(--color-base-100);
border: 1px solid var(--btn-shadow);
}
.is-disabled .choices__list--multiple .choices__item {
background-color: var(--color-neutral);
border: 1px solid var(--btn-shadow);
}
.choices__list--dropdown, .choices__list[aria-expanded] {
background-color: var(--color-base-100);
border: 1px solid var(--btn-shadow);
}
.is-open .choices__list--dropdown, .is-open .choices__list[aria-expanded] {
border-color: var(--btn-shadow);
}
.choices__list--dropdown .choices__item--selectable.is-highlighted, .choices__list[aria-expanded] .choices__item--selectable.is-highlighted {
background-color: var(--color-base-100);
border: 1px solid var(--btn-shadow);
}
.choices__heading {
border-bottom: 1px solid var(--btn-shadow);
color: var(--color-neutral);
}
.choices__input {
background-color: var(--color-base-100);
}
.choices.select {
height: inherit;
padding-inline-start: 0;
}
.choices__inner {
border: 1px solid var(--color-gray-500) !important;
}
.choices__list {
border: none !important;
}
.choices__list--dropdown {
border-left: 1px solid var(--color-gray-500) !important;
border-right: 1px solid var(--color-gray-500) !important;
border-bottom: 1px solid var(--color-gray-500) !important;
border-top: none !important;
}
.choices.select[data-type*="select-one"]::after {
display: none;
}
.choices__inner.bg-secondary {
background-color: var(--color-secondary);
border: none;
z-index: 10;
}
.choices__item.mx-auto.w-max:hover {
background-color: #e2e8f0;
}
.choices__input,
.choices__input--cloned {
width: 100% !important;
}
.choices__list--dropdown span.card-quantity-badge {
display: none;
}

1
static/css/choices.min.css vendored Normal file

File diff suppressed because one or more lines are too long

3
static/css/hovercards.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

View file

@ -0,0 +1 @@
(()=>{function g(n){n.directive("collapse",e),e.inline=(t,{modifiers:i})=>{i.includes("min")&&(t._x_doShow=()=>{},t._x_doHide=()=>{})};function e(t,{modifiers:i}){let r=l(i,"duration",250)/1e3,h=l(i,"min",0),u=!i.includes("min");t._x_isShown||(t.style.height=`${h}px`),!t._x_isShown&&u&&(t.hidden=!0),t._x_isShown||(t.style.overflow="hidden");let c=(d,s)=>{let o=n.setStyles(d,s);return s.height?()=>{}:o},f={transitionProperty:"height",transitionDuration:`${r}s`,transitionTimingFunction:"cubic-bezier(0.4, 0.0, 0.2, 1)"};t._x_transition={in(d=()=>{},s=()=>{}){u&&(t.hidden=!1),u&&(t.style.display=null);let o=t.getBoundingClientRect().height;t.style.height="auto";let a=t.getBoundingClientRect().height;o===a&&(o=h),n.transition(t,n.setStyles,{during:f,start:{height:o+"px"},end:{height:a+"px"}},()=>t._x_isShown=!0,()=>{Math.abs(t.getBoundingClientRect().height-a)<1&&(t.style.overflow=null)})},out(d=()=>{},s=()=>{}){let o=t.getBoundingClientRect().height;n.transition(t,c,{during:f,start:{height:o+"px"},end:{height:h+"px"}},()=>t.style.overflow="hidden",()=>{t._x_isShown=!1,t.style.height==`${h}px`&&u&&(t.style.display="none",t.hidden=!0)})}}}}function l(n,e,t){if(n.indexOf(e)===-1)return t;let i=n[n.indexOf(e)+1];if(!i)return t;if(e==="duration"){let r=i.match(/([0-9]+)ms/);if(r)return r[1]}if(e==="min"){let r=i.match(/([0-9]+)px/);if(r)return r[1]}return i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(g)});})();

5
static/js/alpinejs@3.14.8.min.js vendored Normal file

File diff suppressed because one or more lines are too long

146
static/js/base.js Normal file
View file

@ -0,0 +1,146 @@
/* global window, document, localStorage */
const $ = selector => Array.from(document.querySelectorAll(selector));
const $$ = selector => Array.from(document.querySelector(selector));
(() => {
"use strict";
/**
* Initialize the theme toggle button functionality.
* Toggles between 'dark' and 'light' themes and persists the state in localStorage.
*/
function initThemeToggle() {
const themeToggleButton = document.getElementById("theme-toggle-btn");
if (!themeToggleButton) return;
themeToggleButton.classList.toggle("btn-ghost", !("theme" in localStorage));
themeToggleButton.addEventListener("click", () => {
const documentRoot = document.documentElement;
const isSystemTheme = themeToggleButton.classList.contains("btn-ghost");
const isDarkTheme = documentRoot.classList.contains("dark");
const newTheme = isSystemTheme ? "dark" : (isDarkTheme ? "light" : "system");
if (newTheme === "system") {
documentRoot.classList.toggle("dark", window.matchMedia("(prefers-color-scheme: dark)").matches);
documentRoot.setAttribute("data-theme", window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
localStorage.removeItem("theme");
} else {
if (newTheme === "light") {
documentRoot.classList.remove("dark");
} else if (newTheme === "dark") {
documentRoot.classList.add("dark");
}
documentRoot.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}
themeToggleButton.classList.toggle("btn-ghost", newTheme === "system");
});
}
/**
* Initialize event listeners for forms containing multiselect fields.
* When the form is submitted, process each 'card-multiselect' to create hidden inputs.
*/
function initCardMultiselectHandling() {
const forms = document.querySelectorAll("form");
forms.forEach(form => {
if (form.querySelector("select.card-multiselect")) {
form.addEventListener("submit", () => {
processMultiselectForm(form);
});
}
});
}
/**
* Process multiselect fields within a form before submission by:
* - Creating hidden inputs for each selected option with value in 'card_id:quantity' format.
* - Removing the original name attribute to avoid duplicate submissions.
*
* @param {HTMLFormElement} form - The form element to process.
*/
function processMultiselectForm(form) {
const multiselectFields = form.querySelectorAll("select.card-multiselect");
multiselectFields.forEach(selectField => {
const originalFieldName =
selectField.getAttribute("data-original-name") || selectField.getAttribute("name");
if (!originalFieldName) return;
selectField.setAttribute("data-original-name", originalFieldName);
// Remove any previously generated hidden inputs for this multiselect.
form
.querySelectorAll(`input[data-generated-for-card-multiselect="${originalFieldName}"]`)
.forEach(input => input.remove());
// For each selected option, create a hidden input.
selectField.querySelectorAll("option:checked").forEach(option => {
const cardId = option.value;
const quantity = option.getAttribute("data-quantity") || "1";
const hiddenInput = document.createElement("input");
hiddenInput.type = "hidden";
hiddenInput.name = originalFieldName;
hiddenInput.value = `${cardId}:${quantity}`;
hiddenInput.setAttribute("data-generated-for-card-multiselect", originalFieldName);
form.appendChild(hiddenInput);
});
// Prevent the browser from submitting the select field directly.
selectField.removeAttribute("name");
});
}
/**
* Reset stale selections in all card multiselect fields.
* This is triggered on the window's 'pageshow' event to clear any lingering selections.
*/
function resetCardMultiselectState() {
const multiselectFields = document.querySelectorAll("select.card-multiselect");
multiselectFields.forEach(selectField => {
// Deselect all options.
selectField.querySelectorAll("option").forEach(option => {
option.selected = false;
});
// If the select field has an associated Choices.js instance, clear its selection.
if (selectField.choicesInstance) {
const activeSelections = selectField.choicesInstance.getValue(true);
if (activeSelections.length > 0) {
selectField.choicesInstance.removeActiveItemsByValue(activeSelections);
}
selectField.choicesInstance.setValue([]);
}
});
}
/**
* Process all elements with the 'marquee' class.
* For each element, if its content is overflowing (using isElementOverflowing),
* wrap its innerHTML within a <marquee> tag and remove the 'marquee' class.
*/
function processMarqueeElements() {
document.querySelectorAll('.marquee-calc').forEach(element => {
if (element.offsetWidth >= 148 || element.offsetWidth < element.scrollWidth) {
element.innerHTML = '<marquee behavior="scroll" direction="left" scrolldelay="80">' + element.innerHTML + '</marquee>';
}
element.classList.remove('marquee-calc');
});
}
// Expose processMarqueeElements to be available for AJAX-loaded partial updates.
window.processMarqueeElements = processMarqueeElements;
// On DOMContentLoaded, initialize theme toggling, form processing, and marquee wrapping.
document.addEventListener("DOMContentLoaded", () => {
initThemeToggle();
initCardMultiselectHandling();
processMarqueeElements();
});
// On pageshow, only reset multiselect state if the page was loaded from bfcache.
window.addEventListener("pageshow", function(event) {
if (event.persisted) {
resetCardMultiselectState();
}
});
})();

View file

@ -0,0 +1,219 @@
document.addEventListener('DOMContentLoaded', function() {
if (!window.updateGlobalCardFilters) {
window.updateGlobalCardFilters = function() {
const selects = document.querySelectorAll('.card-multiselect');
// Rebuild global selections and rarity filtering.
const globalSelectedIds = [];
let globalRarity = null;
selects.forEach(select => {
const selectedValues = select.choicesInstance ? select.choicesInstance.getValue(true) : [];
selectedValues.forEach(cardId => {
if (cardId && globalSelectedIds.indexOf(cardId) === -1) {
globalSelectedIds.push(cardId);
}
});
if (selectedValues.length > 0 && globalRarity === null) {
const option = select.querySelector('option[value="${selectedValues[0]}"]');
if (option) {
globalRarity = option.getAttribute('data-rarity');
}
}
});
selects.forEach(select => {
if (select.choicesInstance && select.choicesInstance.dropdown.element) {
// Reset all options to enabled.
select.querySelectorAll('option').forEach(function(option) {
option.disabled = false;
});
// Reset all items to visible.
select.choicesInstance.dropdown.element.querySelectorAll('[data-card-id]').forEach(function(item) {
item.style.display = '';
});
// Filter out options/items that do not match the global rarity.
if (globalRarity) {
select.querySelectorAll('option[data-rarity]:not([data-rarity="'+globalRarity+'"])').forEach(function(option) {
option.disabled = true;
});
select.choicesInstance.dropdown.element.querySelectorAll('[data-rarity]:not([data-rarity="'+globalRarity+'"])').forEach(function(item) {
item.style.display = 'none';
});
}
// Filter out options/items that match the global selected card IDs.
for (const cardId of globalSelectedIds) {
select.choicesInstance.dropdown.element.querySelectorAll('[data-card-id="' + cardId + '"]').forEach(function(item) {
item.style.display = 'none';
});
select.querySelectorAll('option[data-card-id="'+cardId+'"]:not(option[selected])').forEach(function(option) {
option.disabled = true;
});
}
}
});
};
}
if (!window.updateOptionQuantity) {
window.updateOptionQuantity = function(item, quantity) {
const cardId = item.getAttribute('data-card-id');
const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]');
if (option) {
option.setAttribute('data-quantity', quantity);
}
}
}
if (!window.getOptionQuantity) {
window.getOptionQuantity = function(item) {
const cardId = item.getAttribute('data-card-id');
const option = item.closest('.choices__inner').querySelector('option[value="' + cardId + '"]');
return option ? parseInt(option.getAttribute('data-quantity')) : "";
}
}
const selectFields = document.querySelectorAll('.card-multiselect');
selectFields.forEach(selectField => {
const placeholder = selectField.getAttribute('data-placeholder') || '';
const choicesInstance = new Choices(selectField, {
removeItemButton: false,
placeholderValue: placeholder,
searchEnabled: true,
shouldSort: false,
allowHTML: true,
closeDropdownOnSelect: true,
removeItemButton: true,
searchFields: ['label'],
resetScrollPosition: false,
callbackOnCreateTemplates: function(template) {
const getCardContent = (data) => {
let htmlContent = (data.element && data.element.getAttribute('data-html-content')) || data.label;
let quantity = data.element.getAttribute('data-quantity');
quantity = quantity ? parseInt(quantity) : 1;
htmlContent = htmlContent.replace('__QUANTITY__', quantity);
return htmlContent;
};
const renderCard = (classNames, data, type) => {
const rarity = data.element ? data.element.getAttribute('data-rarity') : '';
const cardId = data.element ? data.element.getAttribute('data-card-id') : 0;
const cardname = data.element ? data.element.getAttribute('data-name') : '';
const content = getCardContent(data);
if (type === 'item') {
return template(`
<div class="${classNames.item} mx-auto w-max ${data.highlighted ? classNames.highlightedState : ''} relative"
data-id="${data.id}"
data-card-id="${cardId}"
data-item
data-rarity="${rarity}"
data-name="${cardname}"
aria-selected="true"
style="cursor: pointer; padding: 1rem;">
<button type="button" class="decrement absolute left-[-1.5rem] top-1/2 transform -translate-y-1/2 bg-base-300 text-base-content px-2">-</button>
<button type="button" class="increment absolute right-[-1.5rem] top-1/2 transform -translate-y-1/2 bg-base-300 text-base-content px-2">+</button>
<div class="card-content">${content}</div>
</div>
`);
} else {
const extraAttributes = `data-select-text="${this.config.itemSelectText}" data-choice ${
data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'
}`;
const extraClasses = classNames.itemChoice;
return template(`
<div class="${classNames.item} ${extraClasses} ${data.highlighted ? classNames.highlightedState : ''} mx-auto w-max"
${extraAttributes}
data-id="${data.id}"
data-card-id="${cardId}"
data-name="${cardname}"
data-choice
data-rarity="${rarity}"
style="cursor: pointer;">
${content}
</div>
`);
}
};
return {
choice: function(classNames, data) {
return renderCard(classNames, data, 'choice');
},
item: function(classNames, data) {
return renderCard(classNames, data, 'item');
}
};
}
});
// Associate the Choices instance with the select field.
selectField.choicesInstance = choicesInstance;
if (!window.cardMultiselectInstances) {
window.cardMultiselectInstances = [];
}
window.cardMultiselectInstances.push(selectField);
selectField.addEventListener('change', function() {
if (window.updateGlobalCardFilters) {
window.updateGlobalCardFilters();
}
});
if (choicesInstance.getValue(true).length > 0 && window.updateGlobalCardFilters) {
window.updateGlobalCardFilters();
}
// Listen for increment/decrement clicks (scoped to the choices container).
const choicesContainer = selectField.closest('.choices') || document;
choicesContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('increment')) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const container = e.target.closest('[data-item]');
if (container) {
let quantityBadge = container.querySelector('.card-quantity-badge');
let quantity = window.getOptionQuantity(container);
quantity = quantity + 1;
quantityBadge.innerText = quantity;
window.updateOptionQuantity(container, quantity);
}
}
if (e.target.classList.contains('decrement')) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const container = e.target.closest('[data-item]');
if (container) {
let quantityBadge = container.querySelector('.card-quantity-badge');
let quantity = window.getOptionQuantity(container);
const cardId = container.getAttribute('data-card-id');
if (quantity === 1) {
const option = selectField.querySelector('option[value="' + cardId + '"]');
if (option) {
choicesInstance.removeActiveItemsByValue(option.value);
option.selected = false;
}
if (window.updateGlobalCardFilters) {
window.updateGlobalCardFilters();
}
} else {
quantity = quantity - 1;
quantityBadge.innerText = quantity;
window.updateOptionQuantity(container, quantity);
}
}
}
if (e.target.closest('[data-item]') &&
!e.target.classList.contains('increment') &&
!e.target.classList.contains('decrement')) {
e.stopPropagation();
e.stopImmediatePropagation();
}
});
});
});

2
static/js/choices.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
static/js/hovercards.min.js vendored Normal file

File diff suppressed because one or more lines are too long

123
static/js/tooltip.js Normal file
View file

@ -0,0 +1,123 @@
/**
* tooltip.js
*
* This script uses FloatingUI to create modern, styled tooltips for elements with the
* custom attribute "data-tooltip-html". The tooltips are styled using Tailwind CSS classes
* to support both light and dark themes and include a dynamically positioned arrow.
*
* Make sure the FloatingUIDOM global is available.
* For example, include in your base template:
* <script src="https://unpkg.com/@floating-ui/dom"></script>
*/
const { computePosition, offset, flip, shift, arrow } = FloatingUIDOM;
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-tooltip-html]').forEach((el) => {
let tooltipContainer = null;
let arrowElement = null;
let fadeOutTimeout;
const showTooltip = () => {
if (tooltipContainer) return; // Tooltip already visible
// Retrieve the custom HTML content from the data attribute
const tooltipContent = el.getAttribute('data-tooltip-html');
// Create a container for the tooltip (with modern styling)
tooltipContainer = document.createElement('div');
tooltipContainer.classList.add(
'bg-black', 'text-white',
'shadow-lg', 'rounded-lg', 'p-2',
// Transition classes for simple fade in/out
'transition-opacity', 'duration-200', 'opacity-0'
);
tooltipContainer.style.position = 'absolute';
tooltipContainer.style.zIndex = '9999';
// Set the HTML content for the tooltip
tooltipContainer.innerHTML = '<div class="p-2">' + tooltipContent + '</div>';
// Create the arrow element. The arrow is styled as a small rotated square.
arrowElement = document.createElement('div');
arrowElement.classList.add(
'w-3', 'h-3',
'bg-black',
'transform', 'rotate-45'
);
arrowElement.style.position = 'absolute';
// Append the arrow into the tooltip container
tooltipContainer.appendChild(arrowElement);
// Append the tooltip container to the document body
document.body.appendChild(tooltipContainer);
// Use Floating UI to position the tooltip, including the arrow middleware
computePosition(el, tooltipContainer, {
middleware: [
offset(8),
flip(),
shift({ padding: 5 }),
arrow({ element: arrowElement })
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(tooltipContainer.style, {
left: `${x}px`,
top: `${y}px`
});
// Position the arrow using the arrow middleware data
const { x: arrowX, y: arrowY } = middlewareData.arrow || {};
// Reset any previous inline values
arrowElement.style.left = '';
arrowElement.style.top = '';
arrowElement.style.right = '';
arrowElement.style.bottom = '';
// Adjust the arrow's position according to the placement
if (placement.startsWith('top')) {
arrowElement.style.bottom = '-4px';
arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%';
} else if (placement.startsWith('bottom')) {
arrowElement.style.top = '-4px';
arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%';
} else if (placement.startsWith('left')) {
arrowElement.style.right = '-4px';
arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%';
} else if (placement.startsWith('right')) {
arrowElement.style.left = '-4px';
arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%';
}
});
// Trigger a fade-in by moving from opacity-0 to opacity-100
requestAnimationFrame(() => {
tooltipContainer.classList.remove('opacity-0');
tooltipContainer.classList.add('opacity-100');
});
};
const hideTooltip = () => {
if (tooltipContainer) {
tooltipContainer.classList.remove('opacity-100');
tooltipContainer.classList.add('opacity-0');
// Remove the tooltip from the DOM after the transition duration
fadeOutTimeout = setTimeout(() => {
if (tooltipContainer && tooltipContainer.parentNode) {
tooltipContainer.parentNode.removeChild(tooltipContainer);
}
tooltipContainer = null;
arrowElement = null;
}, 200); // Matches the duration-200 class (200ms)
}
};
// Attach event listeners to show/hide the tooltip
el.addEventListener('mouseenter', showTooltip);
el.addEventListener('mouseleave', hideTooltip);
el.addEventListener('focus', showTooltip);
el.addEventListener('blur', hideTooltip);
});
});