feat: UI Overhaul

This commit is contained in:
2025-12-09 23:49:52 +00:00
parent 68271579bf
commit 07ae21973e
21 changed files with 4546 additions and 86 deletions

2
README.md Normal file
View File

@@ -0,0 +1,2 @@
# MagicBot

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -10,19 +10,44 @@
'modules/socket.js',
'modules/state.js',
'modules/commands.js',
'modules/ui.js',
'modules/decision.js',
'modules/main.js'
'modules/main.js',
// UI Modules (ES Modules required)
'modules/ui/ui_manager.js'
];
// Note: Since ui_manager.js uses ES imports (import ... from ...),
// we MUST load it as type="module".
// Also, core/socket/etc are currently "script" (globals).
// We can leave legacy modules as scripts, but the new UI manager is an ES module.
// Legacy Modules (Synchronous-ish load)
const legacyModules = [
'modules/core.js',
'modules/socket.js',
'modules/state.js',
'modules/commands.js',
'modules/decision.js',
'modules/main.js'
];
modules.forEach(file => {
legacyModules.forEach(file => {
const s = document.createElement('script');
s.src = chrome.runtime.getURL(file);
s.onload = function () {
this.remove();
};
s.onload = function () { this.remove(); };
(document.head || document.documentElement).appendChild(s);
});
// UI Manager (Module)
// Wait a brief moment for legacy modules to be defined on window?
setTimeout(() => {
const s = document.createElement('script');
s.type = 'module';
s.src = chrome.runtime.getURL('modules/ui/ui_manager.js');
s.onload = function () { this.remove(); };
(document.head || document.documentElement).appendChild(s);
}, 500);
console.log('[Magic Bot Extension] Modules injection started.');
})();

View File

@@ -29,9 +29,21 @@
"modules/socket.js",
"modules/state.js",
"modules/commands.js",
"modules/ui.js",
"modules/decision.js",
"modules/main.js",
"modules/ui/ui_styles.js",
"modules/ui/overlays/window_manager.js",
"modules/ui/components/teleport.js",
"modules/ui/components/automation.js",
"modules/ui/components/harvest.js",
"modules/ui/components/diet.js",
"modules/ui/components/visualizers.js",
"modules/ui/overlays/garden.js",
"modules/ui/overlays/inventory.js",
"modules/ui/overlays/shop.js",
"modules/ui/overlays/players.js",
"modules/ui/overlays/pets.js",
"modules/ui/ui_manager.js",
"fullstate.json"
],
"matches": [

View File

@@ -56,6 +56,8 @@ console.log("%c Magic Garden Bot Starting... ", "background: #222; color: #bada5
if (tile && tile.slots) {
tile.slots.forEach((s, idx) => {
if (now >= s.endTime) {
const mutations = s.mutations || [];
// Smart Harvest Logic
if (MB.automation.smartHarvest) {
// 0. Safety: Check for Planted Eggs
@@ -67,7 +69,6 @@ console.log("%c Magic Garden Bot Starting... ", "background: #222; color: #bada5
return;
}
const mutations = s.mutations || [];
const isRainbow = mutations.includes('Rainbow');
const isGold = mutations.includes('Gold');
const isFrozen = mutations.includes('Frozen');

View File

@@ -175,6 +175,9 @@
/* Viewing the file, I see the HTML for chk-auto-feed. I do NOT see the listener in the lines I viewed (up to 120). Listeners are further down. */
/* Assuming listener is missing. */
document.getElementById('chk-auto-feed').onchange = (e) => {
if (MB.automation) MB.automation.autoFeed = e.target.checked;
};
@@ -233,89 +236,11 @@
});
document.getElementById('btn-diet-all').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (!cb.checked) {
cb.click(); // Trigger change event
}
});
};
document.getElementById('btn-diet-none').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (cb.checked) {
cb.click();
}
});
};
document.getElementById('chk-auto-feed').onchange = (e) => {
if (MB.automation) MB.automation.autoFeed = e.target.checked;
};
// Populate Diet List
const populateDietList = () => {
const list = document.getElementById('diet-list');
if (!list) return;
list.innerHTML = '';
// Known crops list - could be dynamic from shop but let's hardcode common ones or try to read from state if available
// Reading from shop state is better if available
let crops = ["Carrot", "Tomato", "Potato", "Corn", "Wheat", "Strawberry", "Blueberry", "Pumpkin", "Watermelon", "Radish", "Cabbage", "Lettuce", "Pepper", "Apple", "Orange", "Banana", "Coconut", "Grape", "Lemon", "Lime", "Peach", "Pear", "Plum", "Cherry", "Mango", "Pineapple", "Aloe", "Daffodil", "OrangeTulip"];
// Try to merge with shop data if present
if (MB.state && MB.state.shops && MB.state.shops.seed && MB.state.shops.seed.inventory) {
const shopCrops = MB.state.shops.seed.inventory.map(i => i.species);
// Union
crops = [...new Set([...crops, ...shopCrops])];
}
crops.sort();
crops.forEach(crop => {
const label = document.createElement('label');
label.style.cssText = "display: flex; align-items: center; gap: 4px; font-size: 10px; cursor: pointer; user-select: none;";
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.cursor = 'pointer';
if (MB.automation && MB.automation.petDiet && MB.automation.petDiet.has(crop)) {
checkbox.checked = true;
}
checkbox.onchange = (e) => {
if (!MB.automation) return;
if (!MB.automation.petDiet) MB.automation.petDiet = new Set();
if (e.target.checked) MB.automation.petDiet.add(crop);
else MB.automation.petDiet.delete(crop);
};
label.appendChild(checkbox);
label.appendChild(document.createTextNode(crop));
list.appendChild(label);
});
};
// Initial populate
setTimeout(populateDietList, 2000); // Wait for state to load maybe?
// Also re-populate on state update if empty?
MB.on('state_updated', () => {
const list = document.getElementById('diet-list');
if (list && list.children.length === 0) populateDietList();
});
document.getElementById('btn-diet-all').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (!cb.checked) {
cb.click(); // Trigger change event
}
});

View File

@@ -0,0 +1,65 @@
import { Styles, createElement } from '../ui_styles.js';
export const Automation = {
init(container) {
const MB = window.MagicBot;
const wrapper = createElement('div', Styles.panel);
// Header
const header = createElement('div', Styles.flexBetween + 'margin-bottom: 5px; cursor: pointer; user-select: none;', {
onclick: () => {
const section = wrapper.querySelector('#section-auto');
const arrow = wrapper.querySelector('#arrow-auto');
if (section.style.display === 'none') {
section.style.display = 'block';
arrow.textContent = '▼';
} else {
section.style.display = 'none';
arrow.textContent = '▶';
}
}
});
header.innerHTML = `
<label style="${Styles.label} cursor: pointer;">Automation</label>
<span id="arrow-auto" style="font-size: 10px; color: #aaa;">▼</span>
`;
const content = createElement('div', 'display: flex; flex-direction: column; gap: 8px;', { id: 'section-auto' });
const createToggle = (label, prop, tooltip = '') => {
const row = createElement('label', Styles.flexBetween + 'cursor: pointer;', { title: tooltip });
const txt = createElement('span', '', { textContent: label });
const chk = createElement('input', 'cursor: pointer;', {
type: 'checkbox',
onchange: (e) => {
if (MB.automation) MB.automation[prop] = e.target.checked;
}
});
// Set initial state if available
if (MB.automation && MB.automation[prop]) chk.checked = true;
row.appendChild(txt);
row.appendChild(chk);
return row;
};
content.appendChild(createToggle('Auto Plant', 'autoPlant'));
content.appendChild(createToggle('Auto Harvest', 'autoHarvest'));
content.appendChild(createToggle('Auto Sell', 'autoSell'));
content.appendChild(createToggle('Auto Feed Pets', 'autoFeed'));
// Smart Harvest Special
const div = createElement('div', 'border-top: 1px solid #444; margin: 4px 0;');
content.appendChild(div);
const smartToggle = createToggle('Smart Harvest', 'smartHarvest', 'Waits for optimal mutations (Gold->Frozen, Long->Wet)');
smartToggle.querySelector('span').style.color = Styles.colors.accent;
content.appendChild(smartToggle);
wrapper.appendChild(header);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
};

View File

@@ -0,0 +1,119 @@
import { Styles, createElement } from '../ui_styles.js';
export const Diet = {
init(container) {
const MB = window.MagicBot;
const wrapper = createElement('div', Styles.panel);
// Header
const header = createElement('div', Styles.flexBetween + 'margin-bottom: 5px; cursor: pointer; user-select: none;', {
onclick: () => {
const section = wrapper.querySelector('#section-diet');
const arrow = wrapper.querySelector('#arrow-diet');
if (section.style.display === 'none') {
section.style.display = 'block';
arrow.textContent = '▼';
} else {
section.style.display = 'none';
arrow.textContent = '▶';
}
}
});
header.innerHTML = `
<label style="${Styles.label} cursor: pointer;">Pet Diet</label>
<span id="arrow-diet" style="font-size: 10px; color: #aaa;">▼</span>
`;
const content = createElement('div', '', { id: 'section-diet' }); // Default open? or closed? Display logic handles it.
// Controls
const controls = createElement('div', 'display: flex; gap: 5px; margin-bottom: 5px;');
const createCtrl = (txt, fn) => {
return createElement('button', Styles.button + 'background: #444; font-size: 10px; padding: 2px 6px; flex: 1;', {
textContent: txt,
onclick: fn
});
};
controls.appendChild(createCtrl('All', () => {
const list = document.getElementById('diet-list');
if (!list) return;
list.querySelectorAll('input').forEach(cb => { if (!cb.checked) cb.click(); });
}));
controls.appendChild(createCtrl('None', () => {
const list = document.getElementById('diet-list');
if (!list) return;
list.querySelectorAll('input').forEach(cb => { if (cb.checked) cb.click(); });
}));
content.appendChild(controls);
// List Container
const listContainer = createElement('div', 'max-height: 150px; overflow-y: auto; background: rgba(0,0,0,0.2); padding: 5px; border-radius: 4px;', {
id: 'diet-list'
});
content.appendChild(listContainer);
wrapper.appendChild(header);
wrapper.appendChild(content);
container.appendChild(wrapper);
// Initial Populate
this.populate(listContainer);
// Subscribe to state updates to refresh list
if (MB.on) {
MB.on('state_updated', () => {
if (listContainer.children.length === 0) this.populate(listContainer);
});
}
},
populate(list) {
const MB = window.MagicBot;
if (!list) return;
// Preserve checks if re-populating not fully implemented but okay for now to clear
list.innerHTML = '';
let crops = ["Carrot", "Tomato", "Potato", "Corn", "Wheat", "Strawberry", "Blueberry", "Pumpkin", "Watermelon", "Radish", "Cabbage", "Lettuce", "Pepper", "Apple", "Orange", "Banana", "Coconut", "Grape", "Lemon", "Lime", "Peach", "Pear", "Plum", "Cherry", "Mango", "Pineapple", "Aloe", "Daffodil", "OrangeTulip"];
// Try to merge with shop data if present
if (MB && MB.state && MB.state.shops && MB.state.shops.seed && MB.state.shops.seed.inventory) {
const shopCrops = MB.state.shops.seed.inventory.map(i => i.species);
crops = [...new Set([...crops, ...shopCrops])];
} else if (MB.state && MB.state.inventory && MB.state.inventory.items) {
// Also scan inventory for seeds
MB.state.inventory.items.forEach(i => {
if (i.itemType === 'Seed' && i.species) crops.push(i.species);
});
crops = [...new Set(crops)];
}
crops.sort();
crops.forEach(crop => {
const label = createElement('label', 'display: flex; align-items: center; gap: 4px; font-size: 10px; cursor: pointer; user-select: none; padding: 2px 0;');
const checkbox = createElement('input', 'cursor: pointer;', { type: 'checkbox' });
if (MB.automation && MB.automation.petDiet && MB.automation.petDiet.has(crop)) {
checkbox.checked = true;
}
checkbox.onchange = (e) => {
if (!MB.automation) return;
if (!MB.automation.petDiet) MB.automation.petDiet = new Set();
if (e.target.checked) MB.automation.petDiet.add(crop);
else MB.automation.petDiet.delete(crop);
};
label.appendChild(checkbox);
label.appendChild(document.createTextNode(crop));
list.appendChild(label);
});
}
};

View File

@@ -0,0 +1,92 @@
import { Styles, createElement } from '../ui_styles.js';
export const Harvest = {
init(container) {
const MB = window.MagicBot;
const wrapper = createElement('div', Styles.panel);
// Header
const header = createElement('div', Styles.flexBetween + 'margin-bottom: 5px; cursor: pointer; user-select: none;', {
onclick: () => {
const section = wrapper.querySelector('#section-hv');
const arrow = wrapper.querySelector('#arrow-hv');
if (section.style.display === 'none') {
section.style.display = 'block';
arrow.textContent = '▼';
} else {
section.style.display = 'none';
arrow.textContent = '▶';
}
}
});
header.innerHTML = `
<label style="${Styles.label} cursor: pointer;">Harvest</label>
<span id="arrow-hv" style="font-size: 10px; color: #aaa;">▼</span>
`;
const content = createElement('div', '', { id: 'section-hv' });
// Range Inputs
const rangeGrid = createElement('div', 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;');
const createInput = (label, id, val) => {
const div = createElement('div');
div.appendChild(createElement('span', 'font-size: 10px; color: #888;', { textContent: label }));
div.appendChild(createElement('input', Styles.input, { type: 'number', id, value: val }));
return div;
};
rangeGrid.appendChild(createInput('Start Slot', 'hv-start', '140'));
rangeGrid.appendChild(createInput('End Slot', 'hv-end', '160'));
content.appendChild(rangeGrid);
// Options Inputs
const optGrid = createElement('div', 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;');
optGrid.appendChild(createInput('Iter. (Count)', 'hv-count', '1'));
optGrid.appendChild(createInput('Delay (ms)', 'hv-delay', '20'));
content.appendChild(optGrid);
// Quick Buttons for Range
const quickRange = createElement('div', 'display: flex; gap: 4px; margin-bottom: 8px;');
const addQuickBtn = (txt, s, e) => {
quickRange.appendChild(createElement('button', Styles.button + 'background: #444; font-size: 9px; padding: 2px 6px; flex: 1;', {
textContent: txt,
onclick: () => {
document.getElementById('hv-start').value = s;
document.getElementById('hv-end').value = e;
}
}));
};
addQuickBtn('All (0-199)', 0, 199);
addQuickBtn('Left (0-99)', 0, 99);
addQuickBtn('Right (100-199)', 100, 199);
content.appendChild(quickRange);
// Actions
const btnRun = createElement('button', Styles.button + 'background: ' + Styles.colors.success + '; width: 100%;', {
textContent: 'Run Harvest',
onclick: () => {
const s = parseInt(document.getElementById('hv-start').value);
const e = parseInt(document.getElementById('hv-end').value);
const c = parseInt(document.getElementById('hv-count').value);
const d = parseInt(document.getElementById('hv-delay').value);
if (MB && MB.harvestLoop) MB.harvestLoop(s, e, c, d);
}
});
content.appendChild(btnRun);
// Sell All (moved here from root)
const btnSell = createElement('button', Styles.button + 'background: ' + Styles.colors.warning + '; width: 100%; margin-top: 10px;', {
textContent: 'Sell All Crops',
onclick: () => {
if (MB && MB.sellAll) MB.sellAll();
}
});
content.appendChild(btnSell);
wrapper.appendChild(header);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
};

View File

@@ -0,0 +1,78 @@
import { Styles, createElement } from '../ui_styles.js';
export const Teleport = {
init(container) {
const MB = window.MagicBot;
const wrapper = createElement('div', Styles.panel);
// Header
const header = createElement('div', Styles.flexBetween + 'margin-bottom: 5px; cursor: pointer; user-select: none;', {
onclick: () => {
const section = wrapper.querySelector('#section-tp');
const arrow = wrapper.querySelector('#arrow-tp');
if (section.style.display === 'none') {
section.style.display = 'block';
arrow.textContent = '▼';
} else {
section.style.display = 'none';
arrow.textContent = '▶';
}
}
});
header.innerHTML = `
<label style="${Styles.label} cursor: pointer;">Teleport</label>
<span id="arrow-tp" style="font-size: 10px; color: #aaa;">▼</span>
`;
// Content
const content = createElement('div', '', { id: 'section-tp' });
const inputsDiv = createElement('div', 'display: flex; gap: 8px;');
const inpX = createElement('input', Styles.input, { type: 'number', placeholder: 'X', value: '15', id: 'tp-x' });
const inpY = createElement('input', Styles.input, { type: 'number', placeholder: 'Y', value: '15', id: 'tp-y' });
const btnGo = createElement('button', Styles.button + 'background: ' + Styles.colors.primary, {
textContent: 'Go',
onclick: () => {
const x = parseInt(inpX.value);
const y = parseInt(inpY.value);
if (MB && MB.teleport) MB.teleport(x, y);
}
});
inputsDiv.appendChild(inpX);
inputsDiv.appendChild(inpY);
inputsDiv.appendChild(btnGo);
content.appendChild(inputsDiv);
// Pre-defined Locations (Bonus feature)
const quickLocs = createElement('div', 'display: flex; gap: 5px; margin-top: 5px;');
const locs = [
{ name: 'Home', x: 15, y: 15 },
{ name: 'Market', x: 50, y: 50 } // Example coordinates, adjust if known
];
locs.forEach(loc => {
const btn = createElement('button', Styles.button + 'background: #333; font-size: 10px; padding: 4px 8px;', {
textContent: loc.name,
onclick: () => {
inpX.value = loc.x;
inpY.value = loc.y;
if (MB && MB.teleport) MB.teleport(loc.x, loc.y);
}
});
quickLocs.appendChild(btn);
});
content.appendChild(quickLocs);
wrapper.appendChild(header);
wrapper.appendChild(content);
container.appendChild(wrapper);
}
};

View File

@@ -0,0 +1,169 @@
import { Styles, createElement } from '../ui_styles.js';
import { WindowManager } from '../overlays/window_manager.js';
export const Visualizers = {
init(container) {
const MB = window.MagicBot;
this.createLogOverlay();
const wrapper = createElement('div', 'margin-top: 10px; border-top: 1px solid #444; padding-top: 10px;');
// Logs and HUDs
const btnRow = createElement('div', 'display: flex; gap: 5px; margin-bottom: 5px;');
const btnLogs = createElement('button', Styles.button + 'flex: 1; background: #5c6bc0;', {
textContent: 'Logs',
onclick: () => {
const el = document.getElementById('magic-bot-logs');
if (el) el.style.display = el.style.display === 'none' ? 'flex' : 'none';
}
});
const btnState = createElement('button', Styles.button + 'flex: 1; background: #ab47bc;', {
textContent: 'All HUDs',
onclick: () => {
if (window.MagicBot_Overlays) {
window.MagicBot_Overlays.openAll();
}
}
});
btnRow.appendChild(btnLogs);
btnRow.appendChild(btnState);
wrapper.appendChild(btnRow);
// Individual toggles for specific useful overlays
const extraRow = createElement('div', 'display: flex; gap: 5px;');
const btnPets = createElement('button', Styles.button + 'flex: 1; background: #ffa726; color: #222;', {
textContent: 'Pets',
onclick: () => {
if (window.MagicBot_Overlays && window.MagicBot_Overlays.openPets) {
window.MagicBot_Overlays.openPets();
}
}
});
const btnModel = createElement('button', Styles.button + 'flex: 1; background: #26a69a; color: #eee;', {
textContent: 'Model',
onclick: () => {
if (window.MagicBot_Overlays && window.MagicBot_Overlays.openModel) {
window.MagicBot_Overlays.openModel();
}
}
});
extraRow.appendChild(btnPets);
extraRow.appendChild(btnModel);
wrapper.appendChild(extraRow);
container.appendChild(wrapper);
// Bind Status Indicator
MB.on('socket_connected', (connected) => {
const el = document.getElementById('mb-status-ind');
if (el) {
el.textContent = connected ? '● Connected' : '● Disconnected';
el.style.color = connected ? Styles.colors.success : Styles.colors.danger;
}
});
MB.on('log', (msg) => {
this.logToOverlay(msg.type, msg.data);
});
},
createLogOverlay() {
if (document.getElementById('magic-bot-logs')) return;
const logOverlay = createElement('div', `
display: none;
position: fixed;
bottom: 20px;
left: 20px;
width: 600px;
height: 300px;
background: rgba(10, 10, 15, 0.95);
border: 1px solid #444;
border-radius: 8px;
z-index: 9999999;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
color: #eee;
flex-direction: column;
box-shadow: 0 4px 15px rgba(0,0,0,0.6);
resize: both;
overflow: hidden;
`, { id: 'magic-bot-logs' });
logOverlay.innerHTML = `
<div style="padding: 5px 10px; background: #222; border-bottom: 1px solid #444; display: flex; justify-content: space-between; align-items: center; cursor: move;" id="log-header">
<span style="font-weight: bold; color: #aaa;">Network Logs</span>
<div style="display:flex; gap:10px; align-items:center;">
<input id="log-filter-input" type="text" placeholder="Filter..." style="background:#333; border:1px solid #555; color:#eee; padding:2px 5px; border-radius:3px; font-size:10px; width:100px;">
<button id="btn-clear-logs" style="background: none; border: none; color: #888; cursor: pointer;">Clear</button>
<button style="background: none; border: none; color: #ff5252; cursor: pointer;" onclick="this.parentElement.parentElement.parentElement.style.display='none'">X</button>
</div>
</div>
<div id="log-content" style="flex: 1; overflow-y: auto; padding: 5px; word-break: break-all;"></div>
`;
document.body.appendChild(logOverlay);
const header = logOverlay.querySelector('#log-header');
let isDragging = false, offX = 0, offY = 0;
header.onmousedown = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
isDragging = true; offX = e.clientX - logOverlay.offsetLeft; offY = e.clientY - logOverlay.offsetTop;
};
window.addEventListener('mousemove', (e) => { if (isDragging) { logOverlay.style.left = (e.clientX - offX) + 'px'; logOverlay.style.top = (e.clientY - offY) + 'px'; } });
window.addEventListener('mouseup', () => isDragging = false);
document.getElementById('btn-clear-logs').onclick = () => {
document.getElementById('log-content').innerHTML = '';
};
// Re-filter on input
document.getElementById('log-filter-input').addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
const lines = document.getElementById('log-content').children;
for (let line of lines) {
line.style.display = line.textContent.toLowerCase().includes(term) ? 'block' : 'none';
}
});
},
logToOverlay(type, data) {
const container = document.getElementById('log-content');
if (!container) return;
const filterInput = document.getElementById('log-filter-input');
const filterTerm = filterInput ? filterInput.value.toLowerCase() : '';
const isAtBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 50;
const line = createElement('div', 'border-bottom: 1px solid #222; padding: 2px 0; cursor: pointer;');
line.title = 'Click to copy';
const timestamp = new Date().toLocaleTimeString();
const color = type === 'TX' ? Styles.colors.success : Styles.colors.primary;
let content = data;
try { if (typeof data === 'string' && (data.startsWith('{') || data.startsWith('['))) content = JSON.stringify(JSON.parse(data)); } catch (e) { }
line.innerHTML = `<span style="color: #666;">[${timestamp}]</span> <span style="color: ${color}; font-weight: bold;">${type}</span> <span style="color: #ccc;">${content}</span>`;
line.onclick = () => {
navigator.clipboard.writeText(data);
line.style.background = 'rgba(255,255,255,0.1)';
setTimeout(() => line.style.background = 'transparent', 200);
};
if (filterTerm && !line.textContent.toLowerCase().includes(filterTerm)) {
line.style.display = 'none';
}
container.appendChild(line);
if (isAtBottom) container.scrollTop = container.scrollHeight;
if (container.children.length > 500) container.removeChild(container.firstChild);
}
};

View File

@@ -0,0 +1,105 @@
import { WindowManager } from './window_manager.js';
export const GardenOverlay = {
open() {
const { win, content } = WindowManager.create('window-garden', '🌻 Garden', { x: 240, y: 20, width: 600, height: 320 });
// Render happens via update()
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-garden-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.garden || !MB.state.garden.tileObjects) {
container.innerHTML = '<div style="color:red">No Garden Data</div>';
return;
}
const now = Date.now();
// Re-render completely for simplicity (virtual DOM would be better but vanilla JS...)
// To avoid flicker, we could diff, but for now just clear and rebuild.
container.innerHTML = '';
// Harvest All Button
const btnHarvestAll = document.createElement('button');
btnHarvestAll.textContent = "Harvest All Ready";
btnHarvestAll.style.cssText = "width: 100%; margin-bottom: 10px; padding: 8px; background: #66bb6a; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer;";
btnHarvestAll.onclick = () => {
let count = 0;
for (let i = 0; i < 200; i++) {
const tile = MB.state.garden.tileObjects[i.toString()];
const slots = tile?.slots || [];
slots.forEach((s, idx) => {
if (Date.now() >= s.endTime) {
MB.sendMsg({ type: 'HarvestCrop', slot: i, slotsIndex: idx, scopePath: ["Room", "Quinoa"] });
count++;
}
});
}
btnHarvestAll.textContent = `Harvested ${count}!`;
setTimeout(() => btnHarvestAll.textContent = "Harvest All Ready", 1000);
};
container.appendChild(btnHarvestAll);
const gridContainer = document.createElement('div');
gridContainer.style.cssText = "display: flex; gap: 10px; justify-content: center;";
const leftGrid = document.createElement('div');
leftGrid.style.cssText = "display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;";
const rightGrid = document.createElement('div');
rightGrid.style.cssText = "display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;";
gridContainer.appendChild(leftGrid);
gridContainer.appendChild(rightGrid);
container.appendChild(gridContainer);
const getProgressColor = (p) => {
const hue = Math.floor(Math.max(0, Math.min(1, p)) * 120);
return `hsl(${hue}, 70%, 50%)`;
};
for (let i = 0; i < 200; i++) {
const tile = MB.state.garden.tileObjects[i.toString()];
const slots = tile?.slots || [];
const crop = slots[0];
const cell = document.createElement('div');
cell.style.cssText = "width: 25px; height: 25px; border: 1px solid #333; border-radius: 3px; background: rgba(255,255,255,0.02); position: relative; cursor: pointer;";
if (crop) {
const totalTime = crop.endTime - crop.startTime;
const progress = totalTime > 0 ? (Math.max(0, now - crop.startTime) / totalTime) : 1;
cell.style.background = getProgressColor(progress);
let anyReady = false;
let tooltip = `Slot: ${i}\nSpecies: ${tile.species}`;
slots.forEach((s, idx) => {
const isReady = now >= s.endTime;
if (isReady) anyReady = true;
const left = Math.max(0, Math.ceil((s.endTime - now) / 1000));
tooltip += `\n#${idx}: ${isReady ? 'READY' : left + 's'}`;
if (s.mutations?.length) tooltip += ` [${s.mutations.join(',')}]`;
});
cell.style.border = `1px solid ${anyReady ? '#fff' : '#555'}`;
cell.title = tooltip;
cell.innerHTML = `<span style="font-size: 10px; color: rgba(0,0,0,0.5); font-weight: bold; position: absolute; top: 1px; left: 2px;">${tile.species[0]}</span>`;
cell.onclick = () => {
slots.forEach((s, idx) => {
if (Date.now() >= s.endTime) {
MB.sendMsg({ type: 'HarvestCrop', slot: i, slotsIndex: idx, scopePath: ["Room", "Quinoa"] });
}
});
cell.style.opacity = '0.5';
};
}
if (i % 20 < 10) leftGrid.appendChild(cell);
else rightGrid.appendChild(cell);
}
}
};

View File

@@ -0,0 +1,54 @@
import { WindowManager } from './window_manager.js';
export const InventoryOverlay = {
open() {
const { win, content } = WindowManager.create('window-inventory', '🎒 Inventory', { x: 560, y: 20, width: 220, height: 300 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-inventory-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.inventory || !MB.state.inventory.items) {
container.innerHTML = '<div style="color:red">No Data</div>';
return;
}
container.innerHTML = '';
const grid = document.createElement('div');
grid.style.cssText = "display: grid; grid-template-columns: 1fr; gap: 5px; font-size: 11px;";
MB.state.inventory.items.forEach(item => {
let name = item.itemType;
let species = item.species || item.parameters?.species || Object.values(item.parameters?.speciesIds || {})[0];
if (species) name = species + " " + item.itemType;
const div = document.createElement('div');
div.style.cssText = "background: rgba(255,255,255,0.05); padding: 4px; border-radius: 4px; display: flex; justify-content: space-between; cursor: pointer;";
div.innerHTML = `<span>${name}</span><span style="color:#aaa; font-weight:bold;">x${item.quantity || item.count || 1}</span>`;
// Plant logic on click
div.onclick = () => {
if (item.itemType === 'Seed' && species) {
// Find empty slot
for (let i = 0; i < 200; i++) {
if (!MB.state.garden.tileObjects[i.toString()]?.slots?.length) {
MB.sendMsg({ type: "PlantSeed", slot: i, species: species, scopePath: ["Room", "Quinoa"] });
div.style.background = 'rgba(100,255,100,0.2)';
setTimeout(() => div.style.background = 'rgba(255,255,255,0.05)', 200);
return;
}
}
alert("Garden Full!");
}
};
grid.appendChild(div);
});
if (MB.state.inventory.items.length === 0) grid.innerHTML = '<div style="color:#666; text-align:center;">Empty</div>';
container.appendChild(grid);
}
};

View File

@@ -0,0 +1,117 @@
import { WindowManager } from './window_manager.js';
import { Styles, createElement } from '../ui_styles.js';
export const ModelOverlay = {
interval: null,
open() {
const { win, content } = WindowManager.create('window-model', '🧠 Model Visualization', { x: 260, y: 340, width: 300, height: 400 });
this.update(content);
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => this.update(content), 1000); // 1s refresh
// Cleanup on close (hacky but works for now if window maps to ID)
const closeBtn = win.querySelector('button'); // THe X button
if (closeBtn) {
const oldClick = closeBtn.onclick;
closeBtn.onclick = () => {
if (this.interval) clearInterval(this.interval);
if (oldClick) oldClick();
}
}
},
update(container) {
if (!container) container = document.getElementById('window-model-content');
if (!container) {
if (this.interval) clearInterval(this.interval);
return;
}
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.inventory) {
container.innerHTML = '<div style="color:red">No State Data</div>';
return;
}
container.innerHTML = '';
const list = createElement('div', 'display: flex; flex-direction: column; gap: 4px; font-size: 10px;');
// Header Info
const header = createElement('div', 'padding: 5px; background: rgba(0,0,0,0.2); margin-bottom: 5px; border-radius: 4px;');
const rainProb = MB.weatherTracker ? (MB.weatherTracker.probabilities.rain * 100).toFixed(0) : '?';
const frostProb = MB.weatherTracker ? (MB.weatherTracker.probabilities.frost * 100).toFixed(0) : '?';
header.innerHTML = `
<div><b>Weather Probabilities:</b> Rain: ${rainProb}% | Frost: ${frostProb}%</div>
<div><b>Smart Harvest:</b> ${MB.automation && MB.automation.smartHarvest ? '<span style="color:#bada55">ON</span>' : '<span style="color:#ff5252">OFF</span>'}</div>
`;
list.appendChild(header);
const plots = MB.state.inventory.items.filter(i => i.itemType === 'Plot');
if (plots.length === 0) {
container.innerHTML += '<div style="padding:10px">No Plots Found</div>';
return;
}
plots.forEach(plot => {
if (!plot.properties) return;
const crop = plot.properties.crop;
if (!crop) return; // Empty plot
const row = createElement('div', 'background: rgba(255,255,255,0.05); padding: 5px; border-radius: 4px; display: flex; flex-direction: column; gap: 2px;');
// Basic Crop Info
const name = crop.name;
const stage = crop.stage;
const totalStages = crop.stages;
const isReady = stage >= totalStages;
// Value Calculation (Simple approximation if actual vals not available)
const sellPrice = crop.sellPrice || 0;
const currentVal = isReady ? sellPrice : 0;
// Decision Logic Inspection
// We can't easily "spy" on the exact decision function result without modifying it to store state,
// but we can replicate the visual indicators.
let statusColor = '#aaa';
let statusText = 'Growing';
if (isReady) {
// Check mutation potential
const nextWeather = MB.weatherTracker ? MB.weatherTracker.nextEvent : 'Unknown';
const hasMutation = crop.mutations && crop.mutations.length > 0;
if (hasMutation) {
statusText = 'Ready (Mutated!)';
statusColor = '#bada55'; // Green
} else {
// This is where we'd ideally show the "Wait vs Harvest" logic
// For now, let's show the current value and a theoretical max if waiting
statusText = 'Ready';
statusColor = '#fff';
}
}
row.innerHTML = `
<div style="font-weight:bold; display:flex; justify-content:space-between;">
<span style="color:${Styles.colors.primary}">${name}</span>
<span style="color:${statusColor}">${statusText}</span>
</div>
<div style="display:flex; justify-content:space-between; color:#888;">
<span>Stage: ${stage}/${totalStages}</span>
<span>Value: ${currentVal}</span>
</div>
<!--
<div style="font-size:9px; color:#666;">
EV: ??? | Boundary: ???
</div>
-->
`;
list.appendChild(row);
});
container.appendChild(list);
}
};

View File

@@ -0,0 +1,86 @@
import { WindowManager } from './window_manager.js';
export const PetsOverlay = {
open() {
// Position next to others
const { win, content } = WindowManager.create('window-pets', '🐾 My Pets', { x: 20, y: 340, width: 220, height: 250 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-pets-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.inventory || !MB.state.inventory.items) {
container.innerHTML = '<div style="color:red">No Data</div>';
return;
}
container.innerHTML = '';
const list = document.createElement('div');
list.style.cssText = "display: flex; flex-direction: column; gap: 8px; font-size: 11px;";
const pets = MB.state.inventory.items.filter(i => i.itemType === 'Pet');
if (pets.length === 0) {
container.innerHTML = '<div style="color:#aaa; text-align:center; padding: 10px;">No Pets Found</div>';
return;
}
pets.forEach(p => {
const row = document.createElement('div');
row.style.cssText = `background: rgba(255,255,255,0.05); padding: 8px; border-radius: 4px; display: flex; flex-direction: column; gap: 2px;`;
// Name/Species
const name = p.name || p.petSpecies;
const species = p.petSpecies;
const isAutoFeed = MB.automation && MB.automation.pets && MB.automation.pets[p.id] && MB.automation.pets[p.id].autoFeed;
const diets = {
'Bee': ['Strawberry', 'Blueberry', 'Tulip', 'Daffodil', 'Lily'],
'Goat': ['Pumpkin', 'Coconut', 'Cactus', 'Pepper']
};
const diet = diets[species] ? diets[species].join(', ') : 'Unknown Diet';
row.innerHTML = `
<div style="font-weight:bold; color: #bada55; display: flex; justify-content: space-between; align-items:center;">
<span>${name}</span>
<label style="font-size:9px; color:#eee; display:flex; align-items:center;">
<input type="checkbox" class="chk-pet-feed" data-id="${p.id}" ${isAutoFeed ? 'checked' : ''}> A-Feed
</label>
</div>
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #666;">
<span>${species}</span>
<span>XP: ${p.xp}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Hunger: ${Math.floor(p.hunger || 0)}</span>
</div>
<div style="color: #aaa; font-size: 9px; margin-top: 4px;">
Diet: ${diet}
</div>
`;
// Event listener for checkbox (needs to be attached after innerHTML reflow)
// We'll trust the parent re-render or attach globally for now to avoid complexity in this simple render loop.
// Actually, let's attach immediately after this loop finishes or use a delegate.
list.appendChild(row);
});
// Delegate listener
list.onchange = (e) => {
if (e.target.classList.contains('chk-pet-feed')) {
const id = e.target.getAttribute('data-id');
if (!MB.automation.pets) MB.automation.pets = {};
if (!MB.automation.pets[id]) MB.automation.pets[id] = {};
MB.automation.pets[id].autoFeed = e.target.checked;
console.log(`[MagicBot] Pet ${id} Auto-Feed: ${e.target.checked}`);
}
};
container.appendChild(list);
}
};
```

View File

@@ -0,0 +1,34 @@
import { WindowManager } from './window_manager.js';
export const PlayersOverlay = {
open() {
const { win, content } = WindowManager.create('window-players', '👥 Players', { x: 20, y: 120, width: 200, height: 200 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-players-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.players) {
container.innerHTML = '<div style="color:red">No Data</div>';
return;
}
container.innerHTML = '';
const list = document.createElement('div');
list.style.cssText = "display: flex; flex-direction: column; gap: 4px; font-size: 11px;";
MB.state.players.forEach(p => {
const isSelf = p.id === MB.state.playerId;
const color = p.isConnected ? '#66bb6a' : '#666';
const row = document.createElement('div');
row.style.cssText = `background: rgba(255,255,255,${isSelf ? '0.1' : '0.05'}); padding: 4px; border-radius: 4px; display: flex; align-items: center; gap: 8px;`;
row.innerHTML = `<div style="width:8px; height:8px; border-radius:50%; background:${color};"></div><span style="color:${isSelf ? '#448aff' : '#eee'}; font-weight:${isSelf ? 'bold' : 'normal'};">${p.name}</span>`;
list.appendChild(row);
});
container.appendChild(list);
}
};

View File

@@ -0,0 +1,50 @@
import { WindowManager } from './window_manager.js';
export const ShopOverlay = {
open() {
const { win, content } = WindowManager.create('window-shop', '🏪 Shop', { x: 790, y: 20, width: 250, height: 350 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-shop-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.shops?.seed) {
container.innerHTML = '<div style="color:red">No Shop Data</div>';
return;
}
container.innerHTML = '';
const grid = document.createElement('div');
grid.style.cssText = "display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; font-size: 11px;";
MB.state.shops.seed.inventory.forEach(item => {
const purchased = MB.state.shopPurchases?.seed?.purchases?.[item.species] || 0;
const stock = Math.max(0, item.initialStock - purchased);
const stockColor = stock > 0 ? '#66bb6a' : '#ff5252';
const auto = MB.automation?.autoBuyItems?.has(item.species);
const div = document.createElement('div');
div.style.cssText = `background: rgba(255,255,255,0.05); padding: 5px; border-radius: 4px; border-left: 2px solid ${stockColor}; border-right: 2px solid ${auto ? '#ffd700' : 'transparent'}; cursor: pointer;`;
div.innerHTML = `<div style="font-weight:bold;">${item.species}</div><div style="color:#888;">${stock}/${item.initialStock}</div>`;
div.oncontextmenu = (e) => {
e.preventDefault();
if (auto) MB.automation.autoBuyItems.delete(item.species);
else MB.automation.autoBuyItems.add(item.species);
this.update(container); // Refresh immediately to show border change
};
div.onclick = () => {
if (stock > 0) MB.sendMsg({ scopePath: ["Room", "Quinoa"], type: "PurchaseSeed", species: item.species });
};
grid.appendChild(div);
});
container.appendChild(grid);
}
};

View File

@@ -0,0 +1,112 @@
export const WindowManager = (function () {
let zIndexCounter = 9999990;
function makeDraggable(el, header) {
let isDragging = false, offX = 0, offY = 0;
header.addEventListener('mousedown', (e) => {
isDragging = true;
offX = e.clientX - el.offsetLeft;
offY = e.clientY - el.offsetTop;
// Bring to front
zIndexCounter++;
el.style.zIndex = zIndexCounter;
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
el.style.left = (e.clientX - offX) + 'px';
el.style.top = (e.clientY - offY) + 'px';
}
});
window.addEventListener('mouseup', () => isDragging = false);
}
function createWindow(id, title, props = {}) {
let win = document.getElementById(id);
if (win) {
// Just bring to front if exists
zIndexCounter++;
win.style.zIndex = zIndexCounter;
return { win, content: document.getElementById(id + '-content') };
}
const x = props.x || 100;
const y = props.y || 100;
const w = props.width || 300;
const h = props.height || 200;
win = document.createElement('div');
win.id = id;
win.style.cssText = `
position: fixed;
top: ${y}px;
left: ${x}px;
width: ${w}px;
height: ${h}px;
background: rgba(10, 10, 20, 0.95);
border: 1px solid #444;
border-radius: 8px;
z-index: ${++zIndexCounter};
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
font-family: sans-serif;
color: #eee;
backdrop-filter: blur(5px);
resize: both;
overflow: hidden;
`;
const header = document.createElement('div');
header.id = id + '-header';
header.style.cssText = `
padding: 8px;
background: rgba(255,255,255,0.05);
border-bottom: 1px solid #444;
cursor: move;
font-weight: bold;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
`;
const titleSpan = document.createElement('span');
titleSpan.textContent = title;
const closeBtn = document.createElement('span');
closeBtn.textContent = '✕';
closeBtn.style.cssText = 'cursor: pointer; color: #ff5252; font-size: 14px;';
closeBtn.onclick = () => win.remove();
header.appendChild(titleSpan);
header.appendChild(closeBtn);
const content = document.createElement('div');
content.id = id + '-content';
content.style.cssText = 'flex: 1; overflow-y: auto; padding: 10px;';
win.appendChild(header);
win.appendChild(content);
document.body.appendChild(win);
makeDraggable(win, header);
// Allow clicking anywhere on window to bring to front
win.addEventListener('mousedown', () => {
if (win.style.zIndex != zIndexCounter) {
zIndexCounter++;
win.style.zIndex = zIndexCounter;
}
});
return { win, content };
}
return {
create: createWindow
};
})();

View File

@@ -0,0 +1,107 @@
import { Styles, createElement } from './ui_styles.js';
import { Teleport } from './components/teleport.js';
import { Automation } from './components/automation.js';
import { Harvest } from './components/harvest.js';
import { Diet } from './components/diet.js';
import { Visualizers } from './components/visualizers.js';
import { GardenOverlay } from './overlays/garden.js';
import { InventoryOverlay } from './overlays/inventory.js';
import { ShopOverlay } from './overlays/shop.js';
import { PlayersOverlay } from './overlays/players.js';
import { PetsOverlay } from './overlays/pets.js';
import { ModelOverlay } from './overlays/model_overlay.js'; // Added
(function () {
const MB = window.MagicBot;
console.log('[MagicBot] Initializing Modular UI...');
// Expose Overlay helpers to global scope for Visualizers to use
window.MagicBot_Overlays = {
openAll: () => {
GardenOverlay.open();
InventoryOverlay.open();
ShopOverlay.open();
PlayersOverlay.open();
PetsOverlay.open();
},
openPets: () => PetsOverlay.open(),
openModel: () => ModelOverlay.open() // Added
};
const UIManager = {
sidebar: null,
isVisible: true,
init() {
this.createSidebar();
this.setupShortcuts();
this.setupUpdateLoop();
console.log('[MagicBot] UI Manager Ready.');
},
createSidebar() {
if (document.getElementById('magic-bot-sidebar')) return;
this.sidebar = createElement('div', Styles.sidebar, { id: 'magic-bot-sidebar' });
// Header
const header = createElement('div', 'padding: 20px 20px 10px 20px; display: flex; justify-content: space-between; align-items: center;');
header.innerHTML = `
<h3 style="margin: 0; color: #bada55; font-size: 18px;">🌱 Magic Bot</h3>
<div id="mb-status-ind" style="font-size: 12px; font-weight: bold; color: #ff5252;">● Disconnected</div>
`;
this.sidebar.appendChild(header);
// Scrollable Content
const content = createElement('div', 'padding: 0 20px 20px 20px; flex: 1; overflow-y: auto;');
// Initialize Components
try { Teleport.init(content); } catch (e) { console.error('Teleport Init Failed', e); }
try { Automation.init(content); } catch (e) { console.error('Automation Init Failed', e); }
try { Harvest.init(content); } catch (e) { console.error('Harvest Init Failed', e); }
try { Diet.init(content); } catch (e) { console.error('Diet Init Failed', e); }
try { Visualizers.init(content); } catch (e) { console.error('Visualizers Init Failed', e); }
this.sidebar.appendChild(content);
document.body.appendChild(this.sidebar);
// Stop Prop on sidebar
['keydown', 'keyup', 'keypress'].forEach(evt =>
this.sidebar.addEventListener(evt, (e) => e.stopPropagation())
);
},
setupShortcuts() {
window.addEventListener('keydown', (e) => {
if (e.code === 'Insert' || e.code === 'Backquote') { // Toggle Key
this.toggleUI();
}
});
},
toggleUI() {
this.isVisible = !this.isVisible;
if (this.sidebar) {
this.sidebar.style.opacity = this.isVisible ? '1' : '0';
this.sidebar.style.pointerEvents = this.isVisible ? 'auto' : 'none';
}
},
setupUpdateLoop() {
// Bind to state updates to refresh overlays
MB.on('state_updated', () => {
GardenOverlay.update();
InventoryOverlay.update();
ShopOverlay.update();
PlayersOverlay.update();
PetsOverlay.update(); // Added
});
}
};
// Initialize
UIManager.init();
})();

View File

@@ -0,0 +1,83 @@
export const Styles = {
// Colors
colors: {
bg: 'rgba(20, 20, 25, 0.95)',
bgPanel: 'rgba(255, 255, 255, 0.05)',
text: '#eee',
textMuted: '#aaa',
primary: '#448aff',
success: '#66bb6a',
danger: '#ff5252',
warning: '#ffa726',
accent: '#bada55',
border: '#444'
},
// Common CSS strings
sidebar: `
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: rgba(20, 20, 25, 0.95);
color: #eee;
padding: 0;
border-radius: 12px;
z-index: 9999999;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-shadow: 0 4px 15px rgba(0,0,0,0.6);
border: 1px solid #444;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
overflow: hidden;
transition: opacity 0.2s, transform 0.2s;
`,
panel: `
background: rgba(255,255,255,0.05);
padding: 10px;
border-radius: 8px;
margin-bottom: 15px;
`,
input: `
width: 100%;
padding: 6px;
background: #333;
border: 1px solid #555;
color: white;
border-radius: 4px;
box-sizing: border-box;
`,
button: `
cursor: pointer;
border: none;
padding: 8px 12px;
border-radius: 4px;
font-weight: bold;
transition: background 0.2s;
color: white;
`,
flexBetween: `
display: flex;
justify-content: space-between;
align-items: center;
`,
label: `
font-size: 12px;
color: #aaa;
margin-bottom: 4px;
display: block;
`
};
export function createElement(tag, style = '', props = {}) {
const el = document.createElement(tag);
el.style.cssText = style;
for (const [k, v] of Object.entries(props)) {
if (k === 'dataset') {
for (const [dk, dv] of Object.entries(v)) el.dataset[dk] = dv;
} else {
el[k] = v;
}
}
return el;
}