Added all elements back to overlay window
Some checks are pending
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run

This commit is contained in:
SwingTheVine 2026-02-15 02:20:21 -05:00
parent 63889a4c02
commit 68bf8b40df
11 changed files with 379 additions and 13 deletions

View file

@ -96,10 +96,76 @@
line-height: 1em;
padding: 0 !important;
}
.bm-button-pin {
vertical-align: middle;
}
.bm-button-pin svg {
width: 50%;
margin: 0 auto;
fill: #111;
}
input[type=number].bm-input-coords {
appearance: auto;
-moz-appearance: textfield;
width: 5.5ch;
margin-left: 1ch;
background-color: rgba(0, 0, 0, 0.2);
padding: 0 0.5ch;
font-size: small;
}
input[type=number].bm-input-coords::-webkit-outer-spin-button,
input[type=number].bm-input-coords::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
div:has(> .bm-input-file) > button {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bm-input-file,
input[type=file] {
display: none !important;
visibility: hidden !important;
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
width: 0 !important;
height: 0 !important;
opacity: 0 !important;
z-index: -9999 !important;
pointer-events: none !important;
}
.bm-window-content {
overflow: hidden;
transition: height 300ms cubic-bezier(.4, 0, .2, 1);
}
.bm-window textarea {
font-size: small;
background-color: rgba(0, 0, 0, 0.2);
padding: 0 0.5ch;
height: 5.25em;
width: 100%;
}
.bm-window small {
font-size: x-small;
color: lightgray;
}
.bm-flex-between {
display: flex;
align-content: center;
justify-content: space-between;
align-items: center;
gap: 0.5ch;
}
.bm-flex-center {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
gap: 0.5ch;
}
#bm-overlay,
#bm-overlay-telemetry {
position: fixed;

View file

@ -2,7 +2,7 @@
// @name Blue Marble
// @name:en Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.88.123
// @version 0.88.133
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @description:en A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
@ -1728,7 +1728,108 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
}
button.textContent = button.textContent == "\u25BC" ? "\u25B6" : "\u25BC";
};
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container" }).addImg({ "class": "bm-favicon", "src": "https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png" }).buildElement().addHeader(1, { "textContent": name }).buildElement().buildElement().addHr().buildElement().buildElement().buildOverlay(document.body);
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container" }).addImg({ "class": "bm-favicon", "src": "https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png" }).buildElement().addHeader(1, { "textContent": name }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container" }).addP({ "id": "bm-user-droplets", "textContent": "Droplets:" }).buildElement().addP({ "id": "bm-user-nextlevel", "textContent": "Next level in..." }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container" }).addDiv({ "class": "bm-container" }).addButton(
{ "class": "bm-button-circle bm-button-pin", "style": "margin-top: 0;", "innerHTML": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 6"><circle cx="2" cy="2" r="2"></circle><path d="M2 6 L3.7 3 L0.3 3 Z"></path><circle cx="2" cy="2" r="0.7" fill="white"></circle></svg></svg>' },
(instance, button) => {
button.onclick = () => {
const coords2 = instance.apiManager?.coordsTilePixel;
if (!coords2?.[0]) {
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
instance.updateInnerHTML("bm-input-tx", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-ty", coords2?.[1] || "");
instance.updateInnerHTML("bm-input-px", coords2?.[2] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[3] || "");
};
}
).buildElement().addInput({ "type": "number", "id": "bm-input-tx", "class": "bm-input-coords", "placeholder": "Tl X", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => {
let splitText = (event.clipboardData || window.clipboardData).getData("text").split(" ").filter((n) => n).map(Number).filter((n) => !isNaN(n));
if (splitText.length !== 4) {
return;
}
let coords2 = selectAllCoordinateInputs(document);
for (let i = 0; i < coords2.length; i++) {
coords2[i].value = splitText[i];
}
event.preventDefault();
});
const handler = () => persistCoords();
input.addEventListener("input", handler);
input.addEventListener("change", handler);
}).buildElement().addInput({ "type": "number", "id": "bm-input-ty", "class": "bm-input-coords", "placeholder": "Tl Y", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener("input", handler);
input.addEventListener("change", handler);
}).buildElement().addInput({ "type": "number", "id": "bm-input-px", "class": "bm-input-coords", "placeholder": "Px X", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener("input", handler);
input.addEventListener("change", handler);
}).buildElement().addInput({ "type": "number", "id": "bm-input-py", "class": "bm-input-coords", "placeholder": "Px Y", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener("input", handler);
input.addEventListener("change", handler);
}).buildElement().buildElement().addDiv({ "class": "bm-container" }).addInputFile({ "class": "bm-input-file", "textContent": "Upload Template", "accept": "image/png, image/jpeg, image/webp, image/bmp, image/gif" }).buildElement().buildElement().addDiv({ "class": "bm-container bm-flex-between" }).addButton({ "textContent": "Enable" }, (instance, button) => {
button.onclick = () => {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(true);
instance.handleDisplayStatus(`Enabled templates!`);
};
}).buildElement().addButton({ "textContent": "Create" }, (instance, button) => {
button.onclick = () => {
const input = document.querySelector("#bm-window-main button.bm-input-file");
const coordTlX = document.querySelector("#bm-input-tx");
if (!coordTlX.checkValidity()) {
coordTlX.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordTlY = document.querySelector("#bm-input-ty");
if (!coordTlY.checkValidity()) {
coordTlY.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordPxX = document.querySelector("#bm-input-px");
if (!coordPxX.checkValidity()) {
coordPxX.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordPxY = document.querySelector("#bm-input-py");
if (!coordPxY.checkValidity()) {
coordPxY.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
if (!input?.files[0]) {
instance.handleDisplayError(`No file selected!`);
return;
}
templateManager.createTemplate(input.files[0], input.files[0]?.name.replace(/\.[^/.]+$/, ""), [Number(coordTlX.value), Number(coordTlY.value), Number(coordPxX.value), Number(coordPxY.value)]);
instance.handleDisplayStatus(`Drew to canvas!`);
};
}).buildElement().addButton({ "textContent": "Disable" }, (instance, button) => {
button.onclick = () => {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(false);
instance.handleDisplayStatus(`Disabled templates!`);
};
}).buildElement().buildElement().addDiv({ "class": "bm-container" }).addTextarea({ "id": overlayMain.outputStatusId, "placeholder": `Status: Sleeping...
Version: ${version}`, "readOnly": true }).buildElement().buildElement().addDiv({ "class": "bm-container bm-flex-between", "style": "margin-bottom: 0;" }).addDiv({ "class": "bm-flex-between" }).addButton(
{ "class": "bm-button-circle", "innerHTML": "\u{1F3A8}", "title": "Template Color Converter" },
(instance, button) => {
button.addEventListener("click", () => {
window.open("https://pepoafonso.github.io/color_converter_wplace/", "_blank", "noopener noreferrer");
});
}
).buildElement().addButton(
{ "class": "bm-button-circle", "innerHTML": "\u{1F310}", "title": "Official Blue Marble Website" },
(instance, button) => {
button.addEventListener("click", () => {
window.open("https://bluemarble.lol/", "_blank", "noopener noreferrer");
});
}
).buildElement().buildElement().addSmall({ "textContent": "Made by SwingTheVine", "style": "margin-top: auto;" }).buildElement().buildElement().buildElement().buildElement().buildElement().buildOverlay(document.body);
}
function buildTelemetryOverlay(overlay) {
overlay.addDiv({ "id": "bm-overlay-telemetry", style: "top: 0px; left: 0px; width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; z-index: 9999;" }).addDiv({ "id": "bm-contain-all-telemetry", style: "display: flex; flex-direction: column; align-items: center;" }).addDiv({ "id": "bm-contain-header-telemetry", style: "margin-top: 10%;" }).addHeader(1, { "textContent": `${name} Telemetry` }).buildElement().buildElement().addDiv({ "id": "bm-contain-telemetry", style: "max-width: 50%; overflow-y: auto; max-height: 80vh;" }).addHr().buildElement().addBr().buildElement().addDiv({ "style": "width: fit-content; margin: auto; text-align: center;" }).addButton({ "id": "bm-button-telemetry-more", "textContent": "More Information" }, (instance, button) => {

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

View file

@ -51,7 +51,7 @@
<a href="https://discord.gg/tpeBPy46hf" target="_blank" rel="noopener noreferrer"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer"><img alt="Blue Marble Website" src="https://img.shields.io/badge/Blue_Marble_Website-crqch-blue?style=flat&logo=globe&logoColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-111hrs_12mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-621-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-631-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Lines of Code" src="https://img.shields.io/badge/Lines_Of_Code-498-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Comments" src="https://img.shields.io/badge/Lines_Of_Comments-498-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Compression" src="https://img.shields.io/badge/Compression-70.19%25-blue"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.88.123",
"version": "0.88.133",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.88.123",
"version": "0.88.133",
"devDependencies": {
"esbuild": "^0.25.0",
"jsdoc": "^4.0.5",

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.88.123",
"version": "0.88.133",
"type": "module",
"homepage": "https://bluemarble.lol/",
"repository": {

View file

@ -2,7 +2,7 @@
// @name Blue Marble
// @name:en Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.88.123
// @version 0.88.133
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @description:en A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine

View file

@ -314,7 +314,121 @@ function buildOverlayMain() {
.addImg({'class': 'bm-favicon', 'src': 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png'}).buildElement()
.addHeader(1, {'textContent': name}).buildElement()
.buildElement()
.addHr()
.addHr().buildElement()
.addDiv({'class': 'bm-container'})
.addP({'id': 'bm-user-droplets', 'textContent': 'Droplets:'}).buildElement()
.addP({'id': 'bm-user-nextlevel', 'textContent': 'Next level in...'}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container'})
.addButton({'class': 'bm-button-circle bm-button-pin', 'style': 'margin-top: 0;', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 6"><circle cx="2" cy="2" r="2"></circle><path d="M2 6 L3.7 3 L0.3 3 Z"></path><circle cx="2" cy="2" r="0.7" fill="white"></circle></svg></svg>'},
(instance, button) => {
button.onclick = () => {
const coords = instance.apiManager?.coordsTilePixel; // Retrieves the coords from the API manager
if (!coords?.[0]) {
instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?');
return;
}
instance.updateInnerHTML('bm-input-tx', coords?.[0] || '');
instance.updateInnerHTML('bm-input-ty', coords?.[1] || '');
instance.updateInnerHTML('bm-input-px', coords?.[2] || '');
instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
}
}
).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-tx', 'class': 'bm-input-coords', 'placeholder': 'Tl X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
//if a paste happens on tx, split and format it into other coordinates if possible
input.addEventListener("paste", (event) => {
let splitText = (event.clipboardData || window.clipboardData).getData("text").split(" ").filter(n => n).map(Number).filter(n => !isNaN(n)); //split and filter all Non Numbers
if (splitText.length !== 4 ) { // If we don't have 4 clean coordinates, end the function.
return;
}
let coords = selectAllCoordinateInputs(document);
for (let i = 0; i < coords.length; i++) {
coords[i].value = splitText[i]; //add the split vales
}
event.preventDefault(); //prevent the pasting of the original paste that would overide the split value
})
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-ty', 'class': 'bm-input-coords', 'placeholder': 'Tl Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-px', 'class': 'bm-input-coords', 'placeholder': 'Px X', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.addInput({'type': 'number', 'id': 'bm-input-py', 'class': 'bm-input-coords', 'placeholder': 'Px Y', 'min': 0, 'max': 2047, 'step': 1, 'required': true}, (instance, input) => {
const handler = () => persistCoords();
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addInputFile({'class': 'bm-input-file', 'textContent': 'Upload Template', 'accept': 'image/png, image/jpeg, image/webp, image/bmp, image/gif'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-flex-between'})
.addButton({'textContent': 'Enable'}, (instance, button) => {
button.onclick = () => {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(true);
instance.handleDisplayStatus(`Enabled templates!`);
}
}).buildElement()
.addButton({'textContent': 'Create'}, (instance, button) => {
button.onclick = () => {
const input = document.querySelector('#bm-window-main button.bm-input-file');
const coordTlX = document.querySelector('#bm-input-tx');
if (!coordTlX.checkValidity()) {coordTlX.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
const coordTlY = document.querySelector('#bm-input-ty');
if (!coordTlY.checkValidity()) {coordTlY.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
const coordPxX = document.querySelector('#bm-input-px');
if (!coordPxX.checkValidity()) {coordPxX.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
const coordPxY = document.querySelector('#bm-input-py');
if (!coordPxY.checkValidity()) {coordPxY.reportValidity(); instance.handleDisplayError('Coordinates are malformed! Did you try clicking on the canvas first?'); return;}
// Kills itself if there is no file
if (!input?.files[0]) {instance.handleDisplayError(`No file selected!`); return;}
templateManager.createTemplate(input.files[0], input.files[0]?.name.replace(/\.[^/.]+$/, ''), [Number(coordTlX.value), Number(coordTlY.value), Number(coordPxX.value), Number(coordPxY.value)]);
instance.handleDisplayStatus(`Drew to canvas!`);
}
}).buildElement()
.addButton({'textContent': 'Disable'}, (instance, button) => {
button.onclick = () => {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(false);
instance.handleDisplayStatus(`Disabled templates!`);
}
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addTextarea({'id': overlayMain.outputStatusId, 'placeholder': `Status: Sleeping...\nVersion: ${version}`, 'readOnly': true}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-flex-between', 'style': 'margin-bottom: 0;'})
.addDiv({'class': 'bm-flex-between'})
// .addButton({'class': 'bm-button-circle', 'innerHTML': '🖌'}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '🎨', 'title': 'Template Color Converter'},
(instance, button) => {
button.addEventListener('click', () => {
window.open('https://pepoafonso.github.io/color_converter_wplace/', '_blank', 'noopener noreferrer');
});
}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '🌐', 'title': 'Official Blue Marble Website'},
(instance, button) => {
button.addEventListener('click', () => {
window.open('https://bluemarble.lol/', '_blank', 'noopener noreferrer');
});
}).buildElement()
.buildElement()
.addSmall({'textContent': 'Made by SwingTheVine', 'style': 'margin-top: auto;'}).buildElement()
.buildElement()
.buildElement()
.buildElement()
.buildElement().buildOverlay(document.body);

View file

@ -128,12 +128,97 @@
padding: 0 !important; /* Overrides the padding in ".bm-window button" */
}
/* Pin button */
.bm-button-pin {
vertical-align: middle;
}
/* Pin button image*/
.bm-button-pin svg {
width: 50%;
margin: 0 auto;
fill: #111;
}
/* Tile (x, y) & Pixel (x, y) input fields */
input[type="number"].bm-input-coords {
appearance: auto;
-moz-appearance: textfield;
width: 5.5ch;
margin-left: 1ch;
background-color: rgba(0, 0, 0, 0.2);
padding: 0 0.5ch;
font-size: small;
}
/* Removes scroll bar on tile & pixel input fields */
input[type="number"].bm-input-coords::-webkit-outer-spin-button,
input[type="number"].bm-input-coords::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* The template file upload button */
div:has(> .bm-input-file) > button {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Force complete invisibility of file input to prevent native browser text */
.bm-input-file,
input[type="file"] {
display: none !important;
visibility: hidden !important;
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
width: 0 !important;
height: 0 !important;
opacity: 0 !important;
z-index: -9999 !important;
pointer-events: none !important;
}
/* Window content container */
.bm-window-content {
overflow: hidden;
transition: height 300ms cubic-bezier(.4, 0, .2, 1);
}
/* Text areas */
.bm-window textarea {
font-size: small;
background-color: rgba(0, 0, 0, 0.2);
padding: 0 0.5ch;
height: 5.25em;
width: 100%;
}
/* Small elements */
.bm-window small {
font-size: x-small;
color: lightgray;
}
/* Flex children space between */
.bm-flex-between {
display: flex;
align-content: center;
justify-content: space-between;
align-items: center;
gap: 0.5ch;
}
/* Flex children space center */
.bm-flex-center {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
gap: 0.5ch;
}