bring remote assets to local

This commit is contained in:
MCHost
2025-06-24 00:32:24 -04:00
parent e3ad40a023
commit 9749663996
28 changed files with 13769 additions and 273 deletions

1178
web/v2/base.css Normal file

File diff suppressed because it is too large Load Diff

810
web/v2/base.js Normal file
View File

@ -0,0 +1,810 @@
/**
* A shortcut for `*.querySelector()`.
* @param {string} id The target query selector
* @param {HTMLElement} ancestor The ancestor element to start from
* @returns {HTMLElement|undefined} The selected element
*/
function $(selector, ancestor = document) {
return ancestor.querySelector(selector);
}
/**
* A shortcut for `*.querySelectorAll()`.
* @param {string} id The target query selector
* @param {HTMLElement} ancestor The ancestor element to start from
* @returns {NodeListOf<any>} The selected elements
*/
function $$(selector, ancestor = document) {
return ancestor.querySelectorAll(selector);
}
/**
* Positions an element at the given coordinates, ensuring it stays within the window.
* @param {HTMLElement} element The element
* @param {number} x Left
* @param {number} y Top
*/
function positionElement(element, x, y) {
const elementWidth = element.offsetWidth;
const elementHeight = element.offsetHeight;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (x + elementWidth > windowWidth) {
x = x - elementWidth;
if (x < 0) x = 0;
}
if (y + elementHeight > windowHeight) {
y = y - elementHeight;
if (y < 0) y = 0;
}
element.style.left = x + 'px';
element.style.top = y + 'px';
}
function isElementVisible(el) {
if (!el || typeof el.getBoundingClientRect !== 'function') {
return false;
}
const rect = el.getBoundingClientRect();
return !!(rect.width || rect.height);
}
/**
* Builds a popup
*/
class PopupBuilder {
#shouldEscapeClose = true;
#elCont; #elTitle; #elBody; #elActions;
#onShow = () => {};
#onHide = () => {};
constructor() {
// Create container
this.#elCont = document.createElement('div');
this.#elCont.classList.add('popupCont');
// Create popup element
this.el = document.createElement('div');
this.el.classList.add('popup');
this.#elCont.appendChild(this.el);
// Add title
this.#elTitle = document.createElement('div');
this.#elTitle.classList.add('title', 'flex-no-shrink');
this.#elTitle.innerText = 'Popup';
this.el.appendChild(this.#elTitle);
// Add body
this.#elBody = document.createElement('div');
this.#elBody.classList.add('body');
this.el.appendChild(this.#elBody);
// Add actions container
this.#elActions = document.createElement('div');
this.#elActions.classList.add('actions', 'flex-no-shrink');
this.el.appendChild(this.#elActions);
// Set up listeners
this.#elCont.addEventListener('click', () => {
if (this.#shouldEscapeClose) this.hide();
});
this.el.addEventListener('click', e => e.stopPropagation());
// Set up focus trap
this.trap = null;
try {
this.trap = focusTrap.createFocusTrap(this.#elCont, {
initialFocus: false,
escapeDeactivates: false
});
} catch(error) {}
}
/**
* Sets the title of the popup.
* @param {string} title The title of the popup
* @returns {PopupBuilder}
*/
setTitle(title) {
this.#elTitle.innerText = title;
return this;
}
/**
* Adds an HTML element to the body of the popup.
* @param {HTMLElement} element The element
* @returns {PopupBuilder}
*/
addBody(element) {
this.#elBody.appendChild(element);
return this;
}
/**
* Appends raw HTML to the body of the popup.
* @param {string} html An HTML string
* @returns {PopupBuilder}
*/
addBodyHTML(html) {
this.#elBody.insertAdjacentHTML('beforeend', html);
return this;
}
/**
* Sets if clicking outside the popup or pressing escape will close the popup. Defaults to `true`.
* @param {boolean} shouldClose
* @returns {PopupBuilder}
*/
setClickOutside(shouldClose) {
this.#shouldEscapeClose = shouldClose;
return this;
}
/**
* Sets a callback function to run when the popup is shown.
* @param {callback} onShow The callback to run
* @returns {PopupBuilder}
*/
setOnShow(onShow) {
this.#onShow = onShow;
return this;
}
/**
* Sets a callback function to run when the popup is hidden.
* @param {callback} onHide The callback to run
* @returns {PopupBuilder}
*/
setOnHide(onHide) {
this.#onHide = onHide;
return this;
}
/**
* Shows the popup.
*/
show() {
document.body.appendChild(this.#elCont);
setTimeout(() => {
this.#elCont.classList.add('visible');
try {
this.trap.activate();
} catch (error) {
console.warn(`Unable to trap focus inside popup. Make sure focus-trap and tabbable are available.`);
}
this.#onShow();
}, 1);
return this;
}
/**
* Hides the popup and removes it from the DOM.
*/
hide() {
this.#elCont.classList.remove('visible');
try {
this.trap.deactivate();
} catch (error) {}
setTimeout(() => {
this.#elCont.parentNode.removeChild(this.#elCont);
}, 200);
this.#onHide();
return this;
}
/**
* @callback PopupBuilderAddActionCallback
* @param {PopupActionBuilder} actionBuilder An action builder to be modified and returned.
*/
/**
* Adds an action button to the bottom of the popup. These buttons are displayed from right to left.
* @param {PopupBuilderAddActionCallback} callback A callback function that returns a `PopupActionBuilder` object. This callback is passed a new `PopupActionBuilder` object that can be modified and returned.
*/
addAction(callback) {
const builder = new PopupActionBuilder(this);
const action = callback(builder);
this.#elActions.appendChild(action.el);
return this;
}
}
/**
* Builds a popup action button
*/
class PopupActionBuilder {
#shouldClose = true;
#onClick = () => {};
/**
* @param {PopupBuilder} parent The parent popup builder.
*/
constructor(parent) {
this.parent = parent;
this.el = document.createElement('button');
this.el.classList.add('btn', 'secondary');
this.el.innerText = `Button`;
this.el.addEventListener('click', () => {
if (this.el.disabled) return;
this.#onClick();
if (this.#shouldClose) parent.hide();
});
}
/**
* Sets whether this action button should use the primary button style. Defaults to `false`.
* @param {boolean} isPrimary
* @returns {PopupActionBuilder}
*/
setIsPrimary(isPrimary) {
if (isPrimary) this.el.classList.remove('secondary');
else this.el.classList.add('secondary');
return this;
}
/**
* Sets whether this action button should use the danger button style. Defaults to `false`.
* @param {boolean} isDanger
* @returns {PopupActionBuilder}
*/
setIsDanger(isDanger) {
if (isDanger) this.el.classList.add('danger');
else this.el.classList.remove('danger');
return this;
}
/**
* Sets whether clicking this action should close the popup. Defaults to `true`.
* @param {boolean} shouldClose
* @returns {PopupActionBuilder}
*/
setShouldClose(shouldClose) {
this.#shouldClose = shouldClose;
return this;
}
/**
* Sets the label of this action button.
* @param {string} label The button's label
* @returns {PopupActionBuilder}
*/
setLabel(label) {
this.el.innerText = label;
return this;
}
/**
* Disables the action button.
* @returns {PopupActionBuilder}
*/
disable() {
this.el.disabled = true;
return this;
}
/**
* Enables the action button.
* @returns {PopupActionBuilder}
*/
enable() {
this.el.disabled = false;
return this;
}
/**
* Sets a callback to be run when this action is clicked.
* @param {callback} onClick The callback
* @returns {PopupActionBuilder}
*/
setClickHandler(onClick) {
this.#onClick = onClick;
return this;
}
}
/**
* Builds a context menu
*/
class ContextMenuBuilder {
#elCont;
constructor() {
// Create container
this.#elCont = document.createElement('div');
this.#elCont.classList.add('contextCont');
// Create popup element
this.el = document.createElement('div');
this.el.classList.add('context');
this.#elCont.appendChild(this.el);
// Hide when clicking outside
this.#elCont.addEventListener('click', () => {
this.hide();
});
this.el.addEventListener('click', (e) => {
e.stopPropagation();
});
// Create focus trap
try {
this.trap = focusTrap.createFocusTrap(this.#elCont, {
initialFocus: false,
escapeDeactivates: false
});
} catch(error) {}
}
/**
* @callback ContextMenuBuilderAddItemCallback
* @param {ContextMenuItemBuilder} itemBuilder An item builder to be modified and returned.
*/
/**
* Adds a clickable item to the context menu.
* @param {ContextMenuBuilderAddItemCallback} callback A callback function that returns a `ContextMenuItemBuilder` object. This callback is passed a new `ContextMenuItemBuilder` object that can be modified and returned.
* @returns {ContextMenuBuilder}
*/
addItem(callback) {
const builder = new ContextMenuItemBuilder(this);
const item = callback(builder);
this.el.appendChild(item.el);
return this;
}
/**
* Adds a separator to the context menu.
* @returns {ContextMenuBuilder}
*/
addSeparator() {
const el = document.createElement('div');
el.classList.add('separator');
this.el.appendChild(el);
return this;
}
/**
* Sets if menu item icons should be shown. If `true`, the space an icon takes up is still visible even on items without set icons. If `false`, all item icons are hidden, regardless of whether they're set or not. Defaults to `true`.
* @param {boolean} areVisible The new state
* @returns {ContextMenuBuilder}
*/
setIconVisibility(areVisible) {
this.el.classList.toggle('hideIcons', !areVisible);
return this;
}
/**
* Shows the context menu at the specified fixed coordinates. This method is used by the other show methods.
* @param {number} x Left
* @param {number} y Top
*/
showAtCoords(x, y) {
document.body.appendChild(this.#elCont);
this.#elCont.classList.remove('ani');
this.#elCont.classList.remove('visible');
setTimeout(() => {
this.el.style.scale = '1';
positionElement(this.el, x, y);
setTimeout(() => {
this.#elCont.classList.add('ani');
setTimeout(() => {
this.#elCont.classList.add('visible');
try {
this.trap.activate();
} catch (error) {
console.warn(`Unable to trap focus inside context menu. Make sure focus-trap and tabbable are available.`);
}
}, 1);
}, 10);
}, 1);
return this;
}
/**
* Shows the context menu originating from the center of an HTML element.
* @param {HTMLElement} el The target element
* @returns {ContextMenuBuilder}
*/
showAtElement(el) {
const rect = el.getBoundingClientRect();
return this.showAtCoords((rect.left+(rect.width/2)), (rect.top+(rect.height/2)));
}
/**
* Shows the context menu at the current mouse position, or fixed in the top left if there hasn't been any mouse movement.
* @returns {ContextMenuBuilder}
*/
showAtCursor() {
return this.showAtCoords(mouse.x+5, mouse.y);
}
/**
* Hides the context menu and removes it from the DOM.
*/
hide() {
this.#elCont.classList.remove('visible');
try {
this.trap.deactivate();
} catch (error) {}
setTimeout(() => {
this.#elCont.parentNode.removeChild(this.#elCont);
}, 200);
return this;
}
}
/**
* Builds a clickable context menu item
*/
class ContextMenuItemBuilder {
#onClick = () => {};
constructor(parent) {
this.parent = parent;
// Create item container
this.el = document.createElement('button');
this.el.classList.add('item');
// Create icon
this.elIcon = document.createElement('div');
this.elIcon.classList.add('icon', 'hidden');
this.el.appendChild(this.elIcon);
// Create label
this.elLabel = document.createElement('div');
this.elLabel.classList.add('label');
this.elLabel.innerText = `Item`;
this.el.appendChild(this.elLabel);
// Set up click handler
this.el.addEventListener('click', () => {
if (this.el.disabled) return;
this.#onClick();
parent.hide();
});
}
/**
* Sets the label for this menu item.
* @param {string} label The item's label
* @returns {ContextMenuItemBuilder}
*/
setLabel(label) {
this.elLabel.innerText = label;
return this;
}
/**
* Sets the icon for this item. If unset, no icon will be shown.
* @param {string} icon A valid [Material Symbol](https://fonts.google.com/icons) string
*
* To make sure you're getting the right symbol, click on the icon, go to the **Android** tab, and copy the string in the code block.
* @returns {ContextMenuItemBuilder}
*/
setIcon(icon) {
this.elIcon.classList.remove('hidden');
this.elIcon.innerText = icon;
return this;
}
/**
* Sets hover tooltip text for this item.
* @param {string} text The tooltip text
* @returns {ContextMenuItemBuilder}
*/
setTooltip(text) {
this.el.title = text;
return this;
}
/**
* Sets whether or not the icon and text of this item should be red.
* @param {boolean} isDanger
* @returns {ContextMenuItemBuilder}
*/
setIsDanger(isDanger) {
this.elIcon.classList.toggle('text-danger', isDanger);
this.elLabel.classList.toggle('text-danger', isDanger);
return this;
}
/**
* Disables this menu item.
* @returns {ContextMenuItemBuilder}
*/
disable() {
this.el.disabled = true;
return this;
}
/**
* Enables this menu item.
* @returns {ContextMenuItemBuilder}
*/
enable() {
this.el.disabled = false;
return this;
}
/**
* Sets a callback to be run when this item is clicked.
* @param {callback} onClick The callback
* @returns {ContextMenuItemBuilder}
*/
setClickHandler(onClick) {
this.#onClick = onClick;
return this;
}
}
/**
* Initializes a toast notification overlay container.
*/
class ToastOverlay {
/**
* @param {('left'|'center'|'right')} hAlign Horizontal alignment
* @param {('top'|'bottom')} vAlign Vertical alignment
*/
constructor(hAlign = 'left', vAlign = 'bottom') {
this.el = document.createElement('div');
this.el.classList.add('toastOverlay', 'col', 'gap-10');
if (hAlign == 'left') this.el.classList.add('align-start');
if (hAlign == 'center') this.el.classList.add('align-center');
if (hAlign == 'right') this.el.classList.add('align-end');
if (vAlign == 'top') this.el.classList.add('justify-start');
if (vAlign == 'bottom') {
this.el.classList.add('justify-start');
this.el.style.flexDirection = 'column-reverse';
}
document.body.appendChild(this.el);
}
/**
* @callback ToastOverlayShowCallback
* @param {ToastBuilder} itemBuilder An item builder to be modified and returned.
*/
/**
* Shows a new toast notification.
* @param {ToastOverlayShowCallback} callback A callback function that returns a `ToastBuilder` object. This callback is passed a new `ToastBuilder` object that can be modified and returned.
* @returns {ToastOverlay}
*/
showToast(callback) {
const builder = new ToastBuilder(this);
const toast = callback(builder);
this.el.insertAdjacentElement('afterbegin', toast.el);
setTimeout(() => {
const delay = toast.el.dataset.delay;
if (delay) setTimeout(() => {
toast.close();
}, delay);
toast.el.classList.add('visible');
if (delay) setTimeout(close, delay);
}, 100);
return this;
}
}
/**
* Builds a toast notification. This class is to be used alongside the `ToastOverlay` class.
*/
class ToastBuilder {
constructor() {
this.el = document.createElement('div');
this.el.classList.add('toast', 'row', 'gap-10', 'align-center');
this.elIcon = document.createElement('div');
this.elIcon.classList.add('icon', 'hidden');
this.el.appendChild(this.elIcon);
this.elBody = document.createElement('div');
this.elBody.classList.add('body');
this.elBody.innerText = 'Toast notification';
this.el.appendChild(this.elBody);
this.elClose = document.createElement('button');
this.elClose.classList.add('btn', 'secondary', 'iconOnly', 'small', 'close');
this.elClose.innerHTML = '<div class="icon">close</div>';
this.elClose.title = 'Dismiss';
this.elClose.addEventListener('click', () => this.close());
this.el.appendChild(this.elClose);
this.el.dataset.delay = 5000;
}
/**
* Sets the icon to show on the left of the toast.
* @param {string} icon A valid [Material Symbol](https://fonts.google.com/icons) string
* @returns {ToastBuilder}
*/
setIcon(icon) {
this.elIcon.classList.remove('hidden');
this.elIcon.innerText = icon;
return this;
}
/**
* Sets the body of the toast.
* @param {string} html The body HTML
* @returns {ToastBuilder}
*/
setBodyHTML(html) {
this.elBody.innerHTML = html;
return this;
}
/**
* Sets the delay before the toast will automatically close after being shown. Set this to `0` to show the toast indefinitely (until the user closes it).
*
* Default is `5000` (5 seconds).
* @param {number} delayMs The delay in milliseconds.
* @returns {ToastBuilder}
*/
setCloseDelay(delayMs) {
this.el.dataset.delay = delayMs;
return this;
}
/**
* Sets whether or not the toast can be closed by the user. Rather, this determines if the close button is visible or not.
* @param {boolean} isCloseable
* @returns {ToastBuilder}
*/
setIsCloseable(isCloseable) {
if (isCloseable) {
this.elClose.style.display = '';
} else {
this.elClose.style.display = 'none';
}
return this;
}
/**
* Closes the toast, assuming that it's visible.
* @returns {ToastBuilder}
*/
close() {
this.el.classList.remove('visible');
setTimeout(() => {
if (!this.el.parentNode) return;
this.el.parentNode.removeChild(this.el);
}, 200);
return this;
}
}
function promptUrlDownload(url, name) {
const a = document.createElement('a');
a.href = url
a.download = name || url.split('/').pop();
a.click();
}
// Create and append the tooltip element
const tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.role = 'tooltip';
tooltip.id = Date.now();
document.body.appendChild(tooltip);
let activeTooltipElement = null;
// Functions for the tooltip
let tooltipTimeout;
const showTooltip = el => {
hideTooltip();
tooltipTimeout = setTimeout(() => {
// After 200ms, remove transitions and reset scale
tooltip.style.transition = 'none';
// Set the tooltip's content
tooltip.innerHTML = el.dataset.tooltip;
setTimeout(() => {
// Position the tooltip and add transitions
positionElement(tooltip, mouse.x+5, mouse.y);
tooltip.style.transition = '';
tooltipTimeout = setTimeout(() => {
// Show the tooltip if the mouse is still over the element
const isMouseOver = el.dataset.isMouseOver === 'true';
if (!isMouseOver || !isElementVisible(el)) return;
tooltip.classList.add('visible');
}, 200);
}, 50);
}, 200);
}
const hideTooltip = () => {
clearTimeout(tooltipTimeout);
tooltip.classList.remove('visible');
};
// Continuously save mouse position
const mouse = { x: 0, y: 0 };
window.addEventListener('mousemove', (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
// Show or hide the tooltip accordingly
if (tooltip.classList.contains('visible')) {
hideTooltip();
}
if (activeTooltipElement) {
showTooltip(activeTooltipElement);
}
});
// Handle dynamic changes
document.addEventListener('domChange', () => {
// Get elements with a title attribute
const titleEls = $$('[title]:not([data-no-tooltip])');
for (const el of titleEls) {
// Remove the title attribute and add it to the tooltip data attribute
const title = el.title;
el.removeAttribute('title');
el.dataset.tooltip = title;
// Add aria label
el.setAttribute('aria-describedby', tooltip.id);
}
// Get elements with a tooltip data attribute
const tooltipEls = $$('[data-tooltip]:not([data-has-tooltip])');
// Loop through 'em
for (const el of tooltipEls) {
el.dataset.isMouseOver = false;
const show = () => {
el.dataset.isMouseOver = true;
activeTooltipElement = el;
};
const hide = () => {
el.dataset.isMouseOver = false;
activeTooltipElement = null;
};
el.addEventListener('mousemove', show);
el.addEventListener('mouseleave', hide);
el.addEventListener('mousedown', hide);
el.addEventListener('mouseup', hide);
// Mark the tooltip as added
el.dataset.hasTooltip = true;
}
// Get slider elements
const sliders = $$('div.slider:not([data-modified])');
// Loop through 'em
for (const slider of sliders) {
// Create elements
const prog = document.createElement('progress');
const input = document.createElement('input');
// Set attributes
let textbox;
const onSliderChange = () => {
// Collect data values
const min = slider.dataset.min || 0;
const max = slider.dataset.max || 100;
const step = slider.dataset.step || 1;
const value = slider.dataset.value || 0;
const rangeId = slider.dataset.rangeId;
const progId = slider.dataset.progId;
textbox = $(slider.dataset.textbox);
// Set attributes
input.type = 'range';
input.min = min;
input.max = max;
input.value = value;
input.step = step;
prog.min = min;
prog.max = max;
prog.value = value;
prog.step = step;
if (progId) prog.id = progId || '';
if (rangeId) input.id = rangeId || '';
// Handle the textbox
if (textbox) {
textbox.type = 'number';
textbox.min = min;
textbox.max = max;
textbox.step = step;
textbox.value = value;
textbox.oninput = () => {
input.value = textbox.value;
input.dispatchEvent(new Event('input'));
};
textbox.onchange = textbox.oninput;
}
// Dispatch events
input.dispatchEvent(new Event('change'));
prog.dispatchEvent(new Event('change'));
}
onSliderChange();
// Append elements
slider.appendChild(prog);
slider.appendChild(input);
// Add event listeners
input.addEventListener('input', () => {
slider.dataset.value = input.value;
prog.value = slider.dataset.value;
if (textbox) textbox.value = slider.dataset.value;
slider.dispatchEvent(new Event('input'));
});
input.addEventListener('change', () => {
slider.dataset.value = input.value;
prog.value = slider.dataset.value;
if (textbox) textbox.value = slider.dataset.value;
});
prog.addEventListener('change', () => {
input.value = slider.dataset.value;
if (textbox) textbox.value = slider.dataset.value;
});
slider.addEventListener('change', onSliderChange);
// Mark the slider as added
slider.dataset.modified = true;
}
// Get expandable textareas
const textareas = $$('textarea[data-make-expandable]:not([data-modified])');
// Loop through 'em
for (const textarea of textareas) {
textarea.addEventListener('resize', () => {
if (!isElementVisible(textarea) || !textarea.scrollHeight) return;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
});
textarea.dispatchEvent(new Event('resize'));
textarea.addEventListener('input', () => {
textarea.dispatchEvent(new Event('resize'));
});
window.addEventListener('resize', () => {
textarea.dispatchEvent(new Event('resize'));
});
textarea.dataset.modified = true;
}
});
// Handle DOM mutations and dispatching the domChange event
const mutationObs = new MutationObserver(() => {
document.dispatchEvent(new Event('domChange'));
});
mutationObs.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
window.addEventListener('domcontentloaded', () => {
document.dispatchEvent(new Event('domChange'));
});

338
web/v2/themes.css Normal file
View File

@ -0,0 +1,338 @@
/*
See `themes.json` for some metadata about each theme.
- The `themeColor` property is the suggested value for the `theme-color` meta tag.
- Equal to the theme's `--b3` color.
- The `name` property is the theme's canonical name.
- The `hue` property is the suggested hue to use for colouring custom elements
*/
.lightblue,
.lightyellow,
.lightgreen,
.lightmuted,
.lightpurple {
/* Background and foreground */
--b0: hsl(180, 60%, 95%);
--b1: hsl(180, 60%, 90%);
--b2: hsl(180, 50%, 85%);
--b3: hsl(180, 40%, 80%);
--b4: hsl(180, 40%, 70%);
--b5: hsl(180, 40%, 60%);
--f4: hsl(180, 30%, 50%);
--f3: hsl(180, 30%, 35%);
--f2: hsl(180, 30%, 15%);
--f1: black;
/* Green accent */
--green0: hsl(120, 60%, 40%);
--green1: hsl(120, 60%, 65%);
--green2: hsl(120, 60%, 80%);
--green3: hsl(120, 60%, 85%);
--green4: hsl(120, 60%, 90%);
--green5: hsl(120, 60%, 95%);
/* Yellow accent */
--yellow0: hsl(40, 100%, 40%);
--yellow1: hsl(40, 100%, 70%);
--yellow2: hsl(40, 100%, 80%);
--yellow3: hsl(40, 100%, 85%);
--yellow4: hsl(40, 100%, 90%);
--yellow5: hsl(40, 100%, 95%);
/* Blue accent */
--blue0: hsl(210, 80%, 45%);
--blue1: hsl(210, 80%, 70%);
--blue2: hsl(210, 80%, 80%);
--blue3: hsl(210, 80%, 85%);
--blue4: hsl(210, 80%, 90%);
--blue5: hsl(210, 80%, 95%);
/* Danger */
--red0: hsl(355, 80%, 55%);
--red1: hsl(355, 80%, 75%);
--red2: hsl(355, 80%, 80%);
--red3: hsl(355, 80%, 85%);
--red4: hsl(355, 80%, 90%);
--red5: hsl(355, 80%, 95%);
/* Code */
--syntax-comment: #6e7781;
--syntax-constant: #0550ae;
--syntax-entity: #8250df;
--syntax-storage-modifier-import: #24292f;
--syntax-entity-tag: #116329;
--syntax-keyword: #cf222e;
--syntax-string: #0a3069;
--syntax-variable: #953800;
}
.lightpurple {
/* Background and foreground */
--b0: hsl(280, 60%, 95%);
--b1: hsl(280, 60%, 90%);
--b2: hsl(280, 60%, 85%);
--b3: hsl(280, 50%, 80%);
--b4: hsl(290, 50%, 70%);
--b5: hsl(300, 40%, 60%);
--f4: hsl(310, 30%, 50%);
--f3: hsl(320, 30%, 35%);
--f2: hsl(320, 30%, 15%);
--f1: black;
}
.lightyellow {
/* Background and foreground */
--b0: hsl(50, 60%, 95%);
--b1: hsl(50, 60%, 90%);
--b2: hsl(40, 60%, 85%);
--b3: hsl(30, 50%, 80%);
--b4: hsl(30, 50%, 70%);
--b5: hsl(20, 40%, 60%);
--f4: hsl(10, 30%, 50%);
--f3: hsl(10, 30%, 35%);
--f2: hsl(0, 30%, 15%);
--f1: black;
}
.lightgreen {
/* Background and foreground */
--b0: hsl(100, 60%, 95%);
--b1: hsl(100, 60%, 90%);
--b2: hsl(110, 60%, 85%);
--b3: hsl(120, 50%, 80%);
--b4: hsl(120, 50%, 70%);
--b5: hsl(130, 40%, 60%);
--f4: hsl(140, 30%, 50%);
--f3: hsl(140, 30%, 35%);
--f2: hsl(150, 30%, 15%);
--f1: black;
--green0: hsl(180, 60%, 50%);
--green1: hsl(180, 60%, 70%);
--green2: hsl(180, 60%, 80%);
--green3: hsl(180, 60%, 85%);
--green4: hsl(180, 60%, 90%);
}
.lightmuted {
/* Background and foreground */
--b0: hsl(160, 10%, 95%);
--b1: hsl(170, 10%, 90%);
--b2: hsl(180, 10%, 85%);
--b3: hsl(190, 10%, 80%);
--b4: hsl(200, 10%, 70%);
--b5: hsl(210, 10%, 60%);
--f4: hsl(220, 10%, 50%);
--f3: hsl(230, 10%, 35%);
--f2: hsl(240, 10%, 15%);
--f1: black;
}
.darkpurple,
.darkyellow,
.darkgreen,
.darkmuted,
.darkblue {
/* Background and foreground */
--b0: hsl(270, 30%, 15%);
--b1: hsl(270, 30%, 20%);
--b2: hsl(270, 30%, 25%);
--b3: hsl(270, 30%, 30%);
--b4: hsl(280, 30%, 40%);
--b5: hsl(290, 30%, 50%);
--f4: hsl(300, 30%, 65%);
--f3: hsl(310, 40%, 80%);
--f2: hsl(310, 50%, 90%);
--f1: white;
/* Green accent */
--green0: hsl(150, 50%, 30%);
--green1: hsl(150, 50%, 40%);
--green2: hsl(150, 50%, 50%);
--green3: hsl(150, 50%, 60%);
--green4: hsl(150, 50%, 70%);
--green5: hsl(150, 50%, 80%);
/* Yellow accent */
--yellow0: hsl(40, 70%, 40%);
--yellow1: hsl(40, 70%, 50%);
--yellow2: hsl(40, 70%, 60%);
--yellow3: hsl(40, 70%, 70%);
--yellow4: hsl(40, 70%, 80%);
--yellow5: hsl(40, 70%, 90%);
/* Blue accent */
--blue0: hsl(240, 60%, 45%);
--blue1: hsl(240, 60%, 55%);
--blue2: hsl(240, 60%, 65%);
--blue3: hsl(240, 60%, 75%);
--blue4: hsl(240, 60%, 85%);
--blue5: hsl(240, 60%, 95%);
/* Danger */
--red0: hsl(340, 55%, 40%);
--red1: hsl(340, 55%, 50%);
--red2: hsl(340, 55%, 60%);
--red3: hsl(340, 55%, 70%);
--red4: hsl(340, 55%, 80%);
--red5: hsl(340, 55%, 90%);
/* Code */
--syntax-comment: #8b949e;
--syntax-constant: #79c0ff;
--syntax-entity: #d2a8ff;
--syntax-storage-modifier-import: #c9d1d9;
--syntax-entity-tag: #7ee787;
--syntax-keyword: #ff7b72;
--syntax-string: #a5d6ff;
--syntax-variable: #ffa657;
}
.darkpurple .textbox,
.darkyellow .textbox,
.darkgreen .textbox,
.darkmuted .textbox,
.darkblue .textbox {
background-color: var(--b0);
border-color: var(--b0);
}
.darkpurple .textbox:focus,
.darkyellow .textbox:focus,
.darkgreen .textbox:focus,
.darkmuted .textbox:focus,
.darkblue .textbox:focus,
.darkpurple .textbox:focus-within,
.darkyellow .textbox:focus-within,
.darkgreen .textbox:focus-within,
.darkmuted .textbox:focus-within,
.darkblue .textbox:focus-within {
background-color: var(--b2);
border-color: var(--f4);
}
.darkpurple .slider input[type="range"],
.darkyellow .slider input[type="range"],
.darkgreen .slider input[type="range"],
.darkmuted .slider input[type="range"],
.darkblue .slider input[type="range"] {
--thumbFill: var(--f2);
--thumbFillHover: var(--f4);
}
.darkpurple .text-info,
.darkyellow .text-info,
.darkgreen .text-info,
.darkmuted .text-info,
.darkblue .text-info {
color: var(--blue2) !important;
}
.darkpurple .text-success,
.darkyellow .text-success,
.darkgreen .text-success,
.darkmuted .text-success,
.darkblue .text-success {
color: var(--green2) !important;
}
.darkpurple .text-warning,
.darkyellow .text-warning,
.darkgreen .text-warning,
.darkmuted .text-warning,
.darkblue .text-warning {
color: var(--yellow2) !important;
}
.darkpurple .text-danger,
.darkyellow .text-danger,
.darkgreen .text-danger,
.darkmuted .text-danger,
.darkblue .text-danger {
color: var(--red2) !important;
}
.darkblue {
/* Background and foreground */
--b0: hsl(230, 30%, 8%);
--b1: hsl(230, 30%, 12%);
--b2: hsl(230, 30%, 18%);
--b3: hsl(230, 30%, 20%);
--b4: hsl(220, 30%, 30%);
--b5: hsl(210, 30%, 45%);
--f4: hsl(200, 30%, 60%);
--f3: hsl(190, 40%, 75%);
--f2: hsl(190, 50%, 85%);
--f1: white;
}
.darkyellow {
/* Background and foreground */
--b0: hsl(40, 20%, 8%);
--b1: hsl(40, 20%, 12%);
--b2: hsl(40, 20%, 18%);
--b3: hsl(40, 30%, 20%);
--b4: hsl(30, 30%, 30%);
--b5: hsl(20, 30%, 45%);
--f4: hsl(10, 30%, 60%);
--f3: hsl(0, 40%, 75%);
--f2: hsl(0, 50%, 85%);
--f1: white;
}
.darkgreen {
/* Background and foreground */
--b0: hsl(170, 20%, 8%);
--b1: hsl(170, 20%, 12%);
--b2: hsl(170, 20%, 18%);
--b3: hsl(170, 30%, 20%);
--b4: hsl(160, 30%, 30%);
--b5: hsl(150, 30%, 45%);
--f4: hsl(140, 30%, 60%);
--f3: hsl(130, 40%, 75%);
--f2: hsl(130, 50%, 85%);
--f1: white;
--green0: hsl(180, 50%, 30%);
--green1: hsl(180, 50%, 40%);
--green2: hsl(180, 50%, 50%);
--green3: hsl(180, 50%, 60%);
--green4: hsl(180, 50%, 70%);
}
.darkmuted {
/* Background and foreground */
--b0: hsl(180, 10%, 5%);
--b1: hsl(190, 10%, 8%);
--b2: hsl(200, 10%, 12%);
--b3: hsl(210, 10%, 15%);
--b4: hsl(220, 10%, 25%);
--b5: hsl(230, 10%, 40%);
--f4: hsl(240, 10%, 55%);
--f3: hsl(250, 10%, 70%);
--f2: hsl(260, 10%, 85%);
--f1: white;
}
.lightblue,
.lightyellow,
.lightgreen,
.lightmuted,
.lightpurple {
/* Luminance palette */
--l0: var(--f1);
--l1: var(--f2);
--l2: var(--f3);
--l3: var(--f4);
--l4: var(--b5);
--l5: var(--b4);
--l6: var(--b3);
--l7: var(--b2);
--l8: var(--b1);
--l9: var(--b0);
}
.darkpurple,
.darkyellow,
.darkgreen,
.darkmuted,
.darkblue {
/* Luminance palette */
--l0: var(--b0);
--l1: var(--b1);
--l2: var(--b2);
--l3: var(--b3);
--l4: var(--b4);
--l5: var(--b5);
--l6: var(--f4);
--l7: var(--f3);
--l8: var(--f2);
--l9: var(--f1);
}