Merge MonacoCSS Plugin directly into Themes as the native panel

Signed-off-by: AvaLilac <257690424+AvaLilac@users.noreply.github.com>
This commit is contained in:
AvaLilac 2026-04-06 18:43:09 -04:00 committed by GitHub
parent 8b0e6588fd
commit dec110d795
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -4,7 +4,8 @@
window.__AVIA_THEMES_LOADED__ = true; window.__AVIA_THEMES_LOADED__ = true;
const STORAGE_KEY = "avia_themes"; const STORAGE_KEY = "avia_themes";
let editingTheme = null; let editingThemeId = null;
let monacoEditorInstance = null;
const TEMPLATE = `/* const TEMPLATE = `/*
@name Whatever name here @name Whatever name here
@ -18,13 +19,26 @@
const getThemes = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); const getThemes = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
const setThemes = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); const setThemes = (data) => localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
function parseMeta(css){ function preloadMonaco() {
return new Promise(resolve => {
if (window.monaco) return resolve();
const loader = document.createElement("script");
loader.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js";
loader.onload = function () {
require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs" } });
require(["vs/editor/editor.main"], () => resolve());
};
document.head.appendChild(loader);
});
}
function parseMeta(css) {
const name = css.match(/@name\s+(.+)/)?.[1] || "Unknown Theme"; const name = css.match(/@name\s+(.+)/)?.[1] || "Unknown Theme";
const author = css.match(/@author\s+(.+)/)?.[1] || "Unknown"; const author = css.match(/@author\s+(.+)/)?.[1] || "Unknown";
const version = css.match(/@version\s+(.+)/)?.[1] || "1.0"; const version = css.match(/@version\s+(.+)/)?.[1] || "1.0";
const rawDescription = css.match(/@description\s+(.+)/)?.[1] || "No Description Available"; const rawDescription = css.match(/@description\s+(.+)/)?.[1] || "No Description Available";
const description = rawDescription.trim() === "*/" ? "No Description Available" : rawDescription; const description = rawDescription.trim() === "*/" ? "No Description Available" : rawDescription;
return {name,author,version,description}; return { name, author, version, description };
} }
function sanitizeFilename(name) { function sanitizeFilename(name) {
@ -47,14 +61,13 @@
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
function applyThemes(){ function applyThemes() {
document.querySelectorAll(".avia-theme-style").forEach(e=>e.remove()); document.querySelectorAll(".avia-theme-style").forEach(e => e.remove());
const themes = getThemes(); getThemes().forEach(theme => {
themes.forEach(theme=>{ if (!theme.enabled) return;
if(!theme.enabled) return; const style = document.createElement("style");
const style=document.createElement("style"); style.className = "avia-theme-style";
style.className="avia-theme-style"; style.textContent = theme.css;
style.textContent=theme.css;
document.head.appendChild(style); document.head.appendChild(style);
}); });
} }
@ -75,209 +88,241 @@
btn.onmouseleave = () => btn.style.opacity = "1"; btn.onmouseleave = () => btn.style.opacity = "1";
} }
function makeDraggable(panel, handle){ function makeDraggable(panel, handle) {
let dragging=false,offsetX,offsetY; let dragging = false, offsetX, offsetY;
handle.addEventListener("mousedown",e=>{ handle.addEventListener("mousedown", e => {
dragging=true; dragging = true;
offsetX=e.clientX-panel.offsetLeft; offsetX = e.clientX - panel.offsetLeft;
offsetY=e.clientY-panel.offsetTop; offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect="none"; document.body.style.userSelect = "none";
}); });
document.addEventListener("mouseup",()=>{dragging=false;document.body.style.userSelect="";}); document.addEventListener("mouseup", () => { dragging = false; document.body.style.userSelect = ""; });
document.addEventListener("mousemove",e=>{ document.addEventListener("mousemove", e => {
if(!dragging) return; if (!dragging) return;
panel.style.left=(e.clientX-offsetX)+"px"; panel.style.left = (e.clientX - offsetX) + "px";
panel.style.top=(e.clientY-offsetY)+"px"; panel.style.top = (e.clientY - offsetY) + "px";
panel.style.right="auto"; panel.style.right = "auto";
panel.style.bottom="auto"; panel.style.bottom = "auto";
}); });
} }
function openThemeEditor(theme){ async function openThemeEditor(themeId) {
editingTheme = theme; await preloadMonaco();
let panel = document.getElementById('avia-theme-editor');
if(panel){ editingThemeId = themeId;
panel.style.display="flex"; const themes = getThemes();
panel.querySelector("textarea").value = theme.css; const theme = themes.find(t => t.id === themeId);
if (!theme) return;
const meta = parseMeta(theme.css);
let panel = document.getElementById("avia-theme-editor");
if (panel) {
panel.style.display = "flex";
panel.querySelector("#avia-theme-editor-title").textContent = "Theme Editor — " + meta.name;
if (monacoEditorInstance) {
monacoEditorInstance._aviaThemeId = themeId;
const model = monacoEditorInstance.getModel();
if (model) model.setValue(theme.css || "");
}
return; return;
} }
panel=document.createElement("div");
panel.id="avia-theme-editor"; panel = document.createElement("div");
Object.assign(panel.style,{ panel.id = "avia-theme-editor";
position:"fixed", Object.assign(panel.style, {
bottom:"24px", position: "fixed",
right:"24px", bottom: "24px",
width:"420px", right: "24px",
height:"340px", width: "650px",
background:"var(--md-sys-color-surface,#1e1e1e)", height: "420px",
color:"var(--md-sys-color-on-surface,#fff)", background: "var(--md-sys-color-surface, #1e1e1e)",
borderRadius:"16px", color: "var(--md-sys-color-on-surface, #fff)",
boxShadow:"0 8px 28px rgba(0,0,0,0.35)", borderRadius: "16px",
zIndex:999999, boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
display:"flex", zIndex: "9999999",
flexDirection:"column", display: "flex",
overflow:"hidden", flexDirection: "column",
border:"1px solid rgba(255,255,255,0.08)", overflow: "hidden",
backdropFilter:"blur(12px)" border: "1px solid rgba(255,255,255,0.08)",
backdropFilter: "blur(12px)"
}); });
const header=document.createElement("div");
header.textContent="Theme Editor"; const header = document.createElement("div");
Object.assign(header.style,{ header.id = "avia-theme-editor-title";
padding:"14px 16px", header.textContent = "Theme Editor — " + meta.name;
fontWeight:"600", Object.assign(header.style, {
fontSize:"14px", padding: "14px 16px",
background:"var(--md-sys-color-surface-container,rgba(255,255,255,0.04))", fontWeight: "600",
borderBottom:"1px solid rgba(255,255,255,0.08)", fontSize: "14px",
cursor:"move" background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor: "move",
color: "#fff",
flex: "0 0 auto"
}); });
makeDraggable(panel,header); makeDraggable(panel, header);
const close=document.createElement("div");
close.textContent="✕"; const close = document.createElement("div");
Object.assign(close.style,{ close.textContent = "✕";
position:"absolute", Object.assign(close.style, {
right:"16px", position: "absolute",
top:"12px", right: "16px",
cursor:"pointer", top: "12px",
opacity:"0.6", cursor: "pointer",
fontSize:"15px", opacity: "0.6",
lineHeight:"1", fontSize: "15px",
padding:"2px 4px" lineHeight: "1",
}); padding: "2px 4px",
close.onmouseenter=()=>close.style.opacity="1"; color: "#fff"
close.onmouseleave=()=>close.style.opacity="0.6";
close.onclick=()=>panel.style.display="none";
const textarea=document.createElement("textarea");
Object.assign(textarea.style,{
flex:"1",
border:"none",
outline:"none",
resize:"none",
padding:"16px",
background:"transparent",
color:"inherit",
fontFamily:"monospace",
fontSize:"13px"
});
textarea.value=theme.css;
textarea.addEventListener("input",()=>{
const themes=getThemes();
const t=themes.find(x=>x.id===editingTheme.id);
if(!t) return;
t.css=textarea.value;
setThemes(themes);
applyThemes();
if(window.__avia_refresh_themes_panel){window.__avia_refresh_themes_panel();}
}); });
close.onmouseenter = () => close.style.opacity = "1";
close.onmouseleave = () => close.style.opacity = "0.6";
close.onclick = () => panel.style.display = "none";
const editorContainer = document.createElement("div");
editorContainer.style.flex = "1";
panel.appendChild(header); panel.appendChild(header);
panel.appendChild(close); panel.appendChild(close);
panel.appendChild(textarea); panel.appendChild(editorContainer);
document.body.appendChild(panel); document.body.appendChild(panel);
monacoEditorInstance = monaco.editor.create(editorContainer, {
value: theme.css || "",
language: "css",
theme: "vs-dark",
automaticLayout: true,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
wordWrap: "on"
});
monacoEditorInstance._aviaThemeId = themeId;
monacoEditorInstance.onDidChangeModelContent(() => {
const id = monacoEditorInstance._aviaThemeId;
if (!id) return;
const value = monacoEditorInstance.getValue();
const all = getThemes();
const target = all.find(t => t.id === id);
if (!target) return;
target.css = value;
setThemes(all);
applyThemes();
header.textContent = "Theme Editor — " + parseMeta(value).name;
if (typeof window.__avia_refresh_themes_panel === "function") {
window.__avia_refresh_themes_panel();
}
});
} }
function toggleThemesPanel(){ function toggleThemesPanel() {
let panel=document.getElementById("avia-themes-panel"); let panel = document.getElementById("avia-themes-panel");
if(panel){ if (panel) {
panel.style.display = panel.style.display==="none"?"flex":"none"; panel.style.display = panel.style.display === "none" ? "flex" : "none";
return; return;
} }
panel=document.createElement("div");
panel.id="avia-themes-panel"; panel = document.createElement("div");
Object.assign(panel.style,{ panel.id = "avia-themes-panel";
position:"fixed", Object.assign(panel.style, {
bottom:"40px", position: "fixed",
right:"40px", bottom: "40px",
width:"500px", right: "40px",
height:"460px", width: "500px",
background:"var(--md-sys-color-surface,#1e1e1e)", height: "460px",
color:"var(--md-sys-color-on-surface,#fff)", background: "var(--md-sys-color-surface, #1e1e1e)",
borderRadius:"16px", color: "var(--md-sys-color-on-surface, #fff)",
boxShadow:"0 8px 28px rgba(0,0,0,0.35)", borderRadius: "16px",
zIndex:999999, boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
display:"flex", zIndex: "999999",
flexDirection:"column", display: "flex",
overflow:"hidden", flexDirection: "column",
border:"1px solid rgba(255,255,255,0.08)", overflow: "hidden",
backdropFilter:"blur(12px)" border: "1px solid rgba(255,255,255,0.08)",
backdropFilter: "blur(12px)"
}); });
const header=document.createElement("div"); const header = document.createElement("div");
header.textContent="Themes"; header.textContent = "Themes";
Object.assign(header.style,{ Object.assign(header.style, {
padding:"14px 16px", padding: "14px 16px",
fontWeight:"600", fontWeight: "600",
fontSize:"14px", fontSize: "14px",
background:"var(--md-sys-color-surface-container,rgba(255,255,255,0.04))", background: "var(--md-sys-color-surface-container, rgba(255,255,255,0.04))",
borderBottom:"1px solid rgba(255,255,255,0.08)", borderBottom: "1px solid rgba(255,255,255,0.08)",
cursor:"move" cursor: "move"
}); });
makeDraggable(panel,header); makeDraggable(panel, header);
const close=document.createElement("div"); const close = document.createElement("div");
close.textContent="✕"; close.textContent = "✕";
Object.assign(close.style,{ Object.assign(close.style, {
position:"absolute", position: "absolute",
right:"16px", right: "16px",
top:"12px", top: "12px",
cursor:"pointer", cursor: "pointer",
opacity:"0.6", opacity: "0.6",
fontSize:"15px", fontSize: "15px",
lineHeight:"1", lineHeight: "1",
padding:"2px 4px" padding: "2px 4px"
}); });
close.onmouseenter=()=>close.style.opacity="1"; close.onmouseenter = () => close.style.opacity = "1";
close.onmouseleave=()=>close.style.opacity="0.6"; close.onmouseleave = () => close.style.opacity = "0.6";
close.onclick=()=>panel.style.display="none"; close.onclick = () => panel.style.display = "none";
const btnRow=document.createElement("div"); const btnRow = document.createElement("div");
Object.assign(btnRow.style,{ Object.assign(btnRow.style, {
display:"flex", display: "flex",
gap:"8px", gap: "8px",
padding:"12px 16px", padding: "12px 16px",
borderBottom:"1px solid rgba(255,255,255,0.08)", borderBottom: "1px solid rgba(255,255,255,0.08)",
flex:"0 0 auto" flex: "0 0 auto"
}); });
const importBtn=document.createElement("button"); const importBtn = document.createElement("button");
importBtn.textContent="Import Theme"; importBtn.textContent = "Import Theme";
styleBtn(importBtn); styleBtn(importBtn);
importBtn.style.flex="1"; importBtn.style.flex = "1";
importBtn.style.padding="8px 12px"; importBtn.style.padding = "8px 12px";
const newBtn=document.createElement("button"); const newBtn = document.createElement("button");
newBtn.textContent="+ New"; newBtn.textContent = "+ New";
styleBtn(newBtn); styleBtn(newBtn);
newBtn.style.flex="1"; newBtn.style.flex = "1";
newBtn.style.padding="8px 12px"; newBtn.style.padding = "8px 12px";
btnRow.appendChild(importBtn); btnRow.appendChild(importBtn);
btnRow.appendChild(newBtn); btnRow.appendChild(newBtn);
const list=document.createElement("div"); const list = document.createElement("div");
Object.assign(list.style,{ Object.assign(list.style, {
flex:"1", flex: "1",
overflowY:"auto", overflowY: "auto",
padding:"16px", padding: "16px",
display:"flex", display: "flex",
flexDirection:"column", flexDirection: "column",
gap:"8px" gap: "8px"
}); });
const dropOverlay=document.createElement("div"); const dropOverlay = document.createElement("div");
dropOverlay.textContent="Drop .css or .txt files here"; dropOverlay.textContent = "Drop .css or .txt files here";
Object.assign(dropOverlay.style,{ Object.assign(dropOverlay.style, {
position:"absolute", position: "absolute",
inset:"0", inset: "0",
background:"rgba(0,0,0,0.6)", background: "rgba(0,0,0,0.6)",
display:"flex", display: "flex",
alignItems:"center", alignItems: "center",
justifyContent:"center", justifyContent: "center",
fontSize:"18px", fontSize: "18px",
fontWeight:"600", fontWeight: "600",
color:"#fff", color: "#fff",
opacity:"0", opacity: "0",
pointerEvents:"none", pointerEvents: "none",
transition:"opacity 0.15s ease", transition: "opacity 0.15s ease",
borderRadius:"16px" borderRadius: "16px"
}); });
panel.appendChild(header); panel.appendChild(header);
@ -319,10 +364,8 @@
dropOverlay.style.opacity = "0"; dropOverlay.style.opacity = "0";
panel.style.border = "1px solid rgba(255,255,255,0.08)"; panel.style.border = "1px solid rgba(255,255,255,0.08)";
dragDepth = 0; dragDepth = 0;
const files = [...e.dataTransfer.files].filter(f => f.name.endsWith(".css") || f.name.endsWith(".txt")); const files = [...e.dataTransfer.files].filter(f => f.name.endsWith(".css") || f.name.endsWith(".txt"));
if (!files.length) return; if (!files.length) return;
const themes = getThemes(); const themes = getThemes();
for (const file of files) { for (const file of files) {
const css = await file.text(); const css = await file.text();
@ -333,84 +376,83 @@
render(); render();
}); });
function render(){ function render() {
list.innerHTML=""; list.innerHTML = "";
const themes=getThemes(); const themes = getThemes();
if(themes.length === 0){ if (themes.length === 0) {
const empty=document.createElement("div"); const empty = document.createElement("div");
empty.textContent="No themes yet. Import or create one above."; empty.textContent = "No themes yet. Import or create one above.";
Object.assign(empty.style,{opacity:"0.4",fontSize:"13px"}); Object.assign(empty.style, { opacity: "0.4", fontSize: "13px" });
list.appendChild(empty); list.appendChild(empty);
return; return;
} }
themes.forEach(theme=>{ themes.forEach(theme => {
const meta=parseMeta(theme.css); const meta = parseMeta(theme.css);
const card=document.createElement("div"); const card = document.createElement("div");
Object.assign(card.style,{ Object.assign(card.style, {
display:"flex", display: "flex",
justifyContent:"space-between", justifyContent: "space-between",
alignItems:"center", alignItems: "center",
padding:"10px 12px", padding: "10px 12px",
borderRadius:"10px", borderRadius: "10px",
background:"rgba(255,255,255,0.04)", background: "rgba(255,255,255,0.04)",
border:"1px solid rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.06)"
marginBottom:"0"
}); });
const left=document.createElement("div"); const left = document.createElement("div");
Object.assign(left.style,{display:"flex",alignItems:"center",gap:"10px"}); Object.assign(left.style, { display: "flex", alignItems: "center", gap: "10px" });
const dot=document.createElement("div"); const dot = document.createElement("div");
Object.assign(dot.style,{ Object.assign(dot.style, {
width:"10px", width: "10px",
height:"10px", height: "10px",
borderRadius:"50%", borderRadius: "50%",
flexShrink:"0", flexShrink: "0",
background: theme.enabled ? "#4dff88" : "#777", background: theme.enabled ? "#4dff88" : "#777",
boxShadow: theme.enabled ? "0 0 6px #4dff88" : "none" boxShadow: theme.enabled ? "0 0 6px #4dff88" : "none"
}); });
const info=document.createElement("div"); const info = document.createElement("div");
info.innerHTML=`<div style="font-weight:600;font-size:13px">${meta.name}</div><div style="font-size:11px;opacity:.5">${meta.author} • v${meta.version}</div><div style="font-size:11px;opacity:.4">${meta.description}</div>`; info.innerHTML = `<div style="font-weight:600;font-size:13px">${meta.name}</div><div style="font-size:11px;opacity:.5">${meta.author} • v${meta.version}</div><div style="font-size:11px;opacity:.4">${meta.description}</div>`;
left.appendChild(dot); left.appendChild(dot);
left.appendChild(info); left.appendChild(info);
const controls=document.createElement("div"); const controls = document.createElement("div");
Object.assign(controls.style,{display:"flex",gap:"6px"}); Object.assign(controls.style, { display: "flex", gap: "6px" });
const toggle=document.createElement("button"); const toggle = document.createElement("button");
toggle.textContent=theme.enabled?"Disable":"Enable"; toggle.textContent = theme.enabled ? "Disable" : "Enable";
styleBtn(toggle); styleBtn(toggle);
toggle.onclick=()=>{ toggle.onclick = () => {
theme.enabled=!theme.enabled; theme.enabled = !theme.enabled;
setThemes(themes); setThemes(themes);
applyThemes(); applyThemes();
render(); render();
}; };
const edit=document.createElement("button"); const edit = document.createElement("button");
edit.textContent="Edit"; edit.textContent = "Edit";
styleBtn(edit, "rgba(100,160,255,0.15)"); styleBtn(edit, "rgba(100,160,255,0.15)");
edit.onclick=()=>openThemeEditor(theme); edit.onclick = () => openThemeEditor(theme.id);
const dlBtn=document.createElement("button"); const dlBtn = document.createElement("button");
dlBtn.textContent="Export"; dlBtn.textContent = "Export";
styleBtn(dlBtn, "rgba(80,200,120,0.15)"); styleBtn(dlBtn, "rgba(80,200,120,0.15)");
dlBtn.title="Download theme as .css"; dlBtn.title = "Download theme as .css";
dlBtn.onclick=(e)=>{ dlBtn.onclick = e => {
e.stopPropagation(); e.stopPropagation();
downloadTheme(theme); downloadTheme(theme);
}; };
const del=document.createElement("button"); const del = document.createElement("button");
del.textContent="✕"; del.textContent = "✕";
styleBtn(del, "rgba(255,80,80,0.15)"); styleBtn(del, "rgba(255,80,80,0.15)");
del.onclick=()=>{ del.onclick = () => {
const updated=themes.filter(t=>t.id!==theme.id); const updated = themes.filter(t => t.id !== theme.id);
setThemes(updated); setThemes(updated);
applyThemes(); applyThemes();
render(); render();
@ -428,18 +470,18 @@
window.__avia_refresh_themes_panel = render; window.__avia_refresh_themes_panel = render;
importBtn.onclick=()=>{ importBtn.onclick = () => {
const input=document.createElement("input"); const input = document.createElement("input");
input.type="file"; input.type = "file";
input.accept=".css,.txt"; input.accept = ".css,.txt";
input.multiple=true; input.multiple = true;
input.onchange=async()=>{ input.onchange = async () => {
const files=[...input.files]; const files = [...input.files];
if(!files.length) return; if (!files.length) return;
const themes=getThemes(); const themes = getThemes();
for(const file of files){ for (const file of files) {
const css=await file.text(); const css = await file.text();
themes.push({id:crypto.randomUUID(),css,enabled:true}); themes.push({ id: crypto.randomUUID(), css, enabled: true });
} }
setThemes(themes); setThemes(themes);
applyThemes(); applyThemes();
@ -448,9 +490,9 @@
input.click(); input.click();
}; };
newBtn.onclick=()=>{ newBtn.onclick = () => {
const themes=getThemes(); const themes = getThemes();
themes.push({id:crypto.randomUUID(),css:TEMPLATE,enabled:true}); themes.push({ id: crypto.randomUUID(), css: TEMPLATE, enabled: true });
setThemes(themes); setThemes(themes);
applyThemes(); applyThemes();
render(); render();
@ -459,21 +501,22 @@
render(); render();
} }
function injectButton(){ function injectButton() {
if(document.getElementById("avia-themes-btn")) return; if (document.getElementById("avia-themes-btn")) return;
const appearanceBtn=[...document.querySelectorAll("a")].find(a=>a.textContent.trim()==="Appearance"); const appearanceBtn = [...document.querySelectorAll("a")].find(a => a.textContent.trim() === "Appearance");
const quickCSS=document.getElementById("stoat-fake-quickcss"); const quickCSS = document.getElementById("stoat-fake-quickcss");
if(!appearanceBtn || !quickCSS) return; if (!appearanceBtn || !quickCSS) return;
const clone=appearanceBtn.cloneNode(true); const clone = appearanceBtn.cloneNode(true);
clone.id="avia-themes-btn"; clone.id = "avia-themes-btn";
const text=[...clone.querySelectorAll("div")].find(d=>d.children.length===0); const text = [...clone.querySelectorAll("div")].find(d => d.children.length === 0);
if(text) text.textContent="(Avia) Themes"; if (text) text.textContent = "(Avia) Themes";
clone.onclick=toggleThemesPanel; clone.onclick = toggleThemesPanel;
quickCSS.parentElement.insertBefore(clone, quickCSS.nextSibling); quickCSS.parentElement.insertBefore(clone, quickCSS.nextSibling);
} }
new MutationObserver(injectButton).observe(document.body,{childList:true,subtree:true}); new MutationObserver(injectButton).observe(document.body, { childList: true, subtree: true });
injectButton(); injectButton();
applyThemes(); applyThemes();
preloadMonaco();
})(); })();