Adds a title management UI to Grok's Imagine carousel. Includes local storage, JSON import/export, live filtering, and sticky controls.
// ==UserScript==
// @name Grok Moniker
// @namespace https://github.com/Zhiro90
// @version 1.0
// @description Adds a title management UI to Grok's Imagine carousel. Includes local storage, JSON import/export, live filtering, and sticky controls.
// @author Zhiro90
// @match *://grok.com/*
// @icon https://grok.com/images/favicon.ico
// @homepageURL https://github.com/Zhiro90/grok-moniker
// @supportURL https://github.com/Zhiro90/grok-moniker/issues
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = "grok_titles_v1";
const LEFT_WIDTH = 220;
let titlesVisible = true;
function loadData() {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
}
function saveData(data) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function enhance() {
const carousel = document.querySelector(".snap-y.snap-mandatory.flex-col");
if (!carousel) return;
if (!carousel.dataset.expanded) {
carousel.dataset.expanded = "true";
carousel.style.boxSizing = "content-box";
if (titlesVisible) {
carousel.style.paddingLeft = `${LEFT_WIDTH}px`;
carousel.style.marginLeft = `-${LEFT_WIDTH}px`;
}
}
addTopControls(carousel);
const data = loadData();
const activeFilter = carousel.querySelector(".grok-controls input")?.value.toLowerCase() || "";
const buttons = Array.from(carousel.querySelectorAll("button"))
.filter(btn => btn.querySelector("img"));
buttons.forEach(btn => {
if (btn.querySelector(".grok-title-ui")) return;
const img = btn.querySelector("img");
if (!img) return;
const id = img.src;
if (activeFilter) {
const titleText = (data[id] || "").toLowerCase();
if (!titleText.includes(activeFilter)) {
btn.style.display = "none";
}
}
btn.style.overflow = "visible";
img.style.borderRadius = getComputedStyle(btn).borderRadius || "8px";
const titleCol = document.createElement("div");
titleCol.className = "grok-title-ui";
titleCol.style.position = "absolute";
titleCol.style.right = "100%";
titleCol.style.top = "50%";
titleCol.style.transform = "translateY(-50%)";
titleCol.style.width = LEFT_WIDTH + "px";
titleCol.style.display = titlesVisible ? "flex" : "none";
titleCol.style.alignItems = "center";
titleCol.style.justifyContent = "flex-end";
titleCol.style.paddingRight = "12px";
titleCol.style.zIndex = "100";
titleCol.onclick = e => e.stopPropagation();
titleCol.onmousedown = e => e.stopPropagation();
function render() {
titleCol.innerHTML = "";
if (!data[id]) {
const plus = document.createElement("div");
plus.textContent = "+";
plus.style.cursor = "pointer";
plus.style.background = "#222";
plus.style.color = "#ddd";
plus.style.borderRadius = "8px";
plus.style.width = "36px";
plus.style.height = "36px";
plus.style.display = "flex";
plus.style.alignItems = "center";
plus.style.justifyContent = "center";
plus.onclick = (e) => { e.preventDefault(); e.stopPropagation(); edit(); };
titleCol.appendChild(plus);
return;
}
const pill = document.createElement("div");
pill.style.display = "flex";
pill.style.alignItems = "center";
pill.style.gap = "8px";
pill.style.padding = "6px 12px";
pill.style.background = "rgba(40, 40, 40, 0.9)";
pill.style.border = "1px solid #444";
pill.style.borderRadius = "999px";
pill.style.maxWidth = (LEFT_WIDTH - 20) + "px";
pill.style.overflow = "hidden";
const text = document.createElement("span");
text.textContent = data[id];
text.style.fontWeight = "500";
text.style.fontFamily = "system-ui, -apple-system, sans-serif";
text.style.fontSize = "13px";
text.style.color = "#fff";
text.style.letterSpacing = "0.2px";
text.style.whiteSpace = "nowrap";
text.style.overflow = "hidden";
text.style.textOverflow = "ellipsis";
const editBtn = document.createElement("span");
editBtn.textContent = "✎";
editBtn.style.cursor = "pointer";
editBtn.style.opacity = "0.7";
editBtn.onmouseenter = () => editBtn.style.opacity = "1";
editBtn.onmouseleave = () => editBtn.style.opacity = "0.7";
editBtn.onclick = e => { e.preventDefault(); e.stopPropagation(); edit(); };
const delBtn = document.createElement("span");
delBtn.textContent = "✕";
delBtn.style.cursor = "pointer";
delBtn.style.color = "#ff6b6b";
delBtn.style.opacity = "0.7";
delBtn.onmouseenter = () => delBtn.style.opacity = "1";
delBtn.onmouseleave = () => delBtn.style.opacity = "0.7";
delBtn.onclick = e => {
e.preventDefault();
e.stopPropagation();
delete data[id];
saveData(data);
render();
};
pill.appendChild(text);
pill.appendChild(editBtn);
pill.appendChild(delBtn);
titleCol.appendChild(pill);
}
function edit() {
titleCol.innerHTML = "";
const input = document.createElement("input");
input.type = "text";
input.value = data[id] || "";
input.style.width = (LEFT_WIDTH - 20) + "px";
input.style.fontSize = "13px";
input.style.fontWeight = "500";
input.style.padding = "6px 10px";
input.style.borderRadius = "8px";
input.style.border = "1px solid #666";
input.style.background = "#111";
input.style.color = "#fff";
input.style.outline = "none";
// MEJORA: Evitar que guarde dos veces seguidas si presionas Enter y luego se dispara el onblur
let isSaved = false;
function commitSave() {
if (isSaved) return;
isSaved = true;
const val = input.value.trim();
if (val) data[id] = val;
else delete data[id];
saveData(data);
render();
}
input.onkeydown = e => {
e.stopPropagation();
if (e.key === "Enter") {
commitSave();
} else if (e.key === "Escape") {
isSaved = true;
render();
}
};
// MEJORA: Guarda al dar clic fuera de la caja
input.onblur = commitSave;
input.onclick = e => { e.preventDefault(); e.stopPropagation(); };
titleCol.appendChild(input);
input.focus();
}
render();
btn.appendChild(titleCol);
});
}
function addTopControls(carousel) {
if (carousel.querySelector(".grok-controls")) return;
const controls = document.createElement("div");
controls.className = "grok-controls";
// MEJORA: Propiedades "Sticky" para que el encabezado no se pierda al hacer scroll
controls.style.position = "sticky";
controls.style.top = "0px";
controls.style.zIndex = "200"; // Tiene que estar por encima de todo el carrusel
controls.style.background = "rgba(10, 10, 10, 0.95)"; // Fondo semitransparente oscuro
controls.style.backdropFilter = "blur(8px)"; // Da un efecto de cristal/acrílico
controls.style.padding = "8px 0";
controls.style.margin = "0";
controls.style.display = "flex";
controls.style.alignItems = "center";
controls.style.justifyContent = titlesVisible ? "flex-end" : "center";
controls.style.gap = "8px";
controls.style.width = "100%";
controls.style.paddingRight = titlesVisible ? "4px" : "0px";
// Filtro en vivo
const filterInput = document.createElement("input");
filterInput.type = "text";
filterInput.placeholder = "🔍 Filter...";
filterInput.style.flexGrow = "1";
filterInput.style.maxWidth = "130px";
filterInput.style.background = "#222";
filterInput.style.border = "1px solid #444";
filterInput.style.color = "#fff";
filterInput.style.borderRadius = "6px";
filterInput.style.padding = "4px 8px";
filterInput.style.fontSize = "12px";
filterInput.style.outline = "none";
filterInput.style.display = titlesVisible ? "block" : "none";
filterInput.onkeydown = e => e.stopPropagation();
filterInput.oninput = (e) => {
e.stopPropagation();
const term = filterInput.value.toLowerCase();
const data = loadData();
const buttons = Array.from(carousel.querySelectorAll("button")).filter(btn => btn.querySelector("img"));
buttons.forEach(btn => {
const img = btn.querySelector("img");
if (!img) return;
const title = (data[img.src] || "").toLowerCase();
if (term === "" || title.includes(term)) {
btn.style.display = "";
} else {
btn.style.display = "none";
}
});
};
// Botón Importar Archivo JSON
const importBtn = document.createElement("button");
importBtn.innerHTML = "📥";
importBtn.title = "Import data (JSON)";
importBtn.style.fontSize = "16px";
importBtn.style.background = "transparent";
importBtn.style.border = "none";
importBtn.style.cursor = "pointer";
importBtn.style.opacity = "0.6";
importBtn.style.display = titlesVisible ? "inline-block" : "none";
importBtn.onmouseenter = () => importBtn.style.opacity = "1";
importBtn.onmouseleave = () => importBtn.style.opacity = "0.6";
importBtn.onclick = () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'application/json';
fileInput.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
try {
JSON.parse(ev.target.result);
localStorage.setItem(STORAGE_KEY, ev.target.result);
alert("Titles imported successfully! The page will reload to apply changes.");
location.reload();
} catch (err) {
alert("Error: The file is not a valid JSON.");
}
};
reader.readAsText(file);
};
fileInput.click();
};
// Botón Exportar Archivo JSON
const exportBtn = document.createElement("button");
exportBtn.innerHTML = "📤";
exportBtn.title = "Export data (JSON)";
exportBtn.style.fontSize = "16px";
exportBtn.style.background = "transparent";
exportBtn.style.border = "none";
exportBtn.style.cursor = "pointer";
exportBtn.style.opacity = "0.6";
exportBtn.style.display = titlesVisible ? "inline-block" : "none";
exportBtn.onmouseenter = () => exportBtn.style.opacity = "1";
exportBtn.onmouseleave = () => exportBtn.style.opacity = "0.6";
exportBtn.onclick = () => {
const dataStr = localStorage.getItem(STORAGE_KEY) || "{}";
const blob = new Blob([dataStr], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const date = new Date().toISOString().split('T')[0];
a.download = `grok_titles_${date}.json`;
a.click();
URL.revokeObjectURL(url);
};
// Botón Ocultar/Mostrar Títulos
const toggleBtn = document.createElement("button");
toggleBtn.innerHTML = titlesVisible ? "👁️" : "🙈";
toggleBtn.title = "Toggle titles visibility";
toggleBtn.style.fontSize = "16px";
toggleBtn.style.background = "transparent";
toggleBtn.style.border = "none";
toggleBtn.style.cursor = "pointer";
toggleBtn.style.opacity = titlesVisible ? "0.6" : "1";
toggleBtn.onmouseenter = () => toggleBtn.style.opacity = "1";
toggleBtn.onmouseleave = () => toggleBtn.style.opacity = titlesVisible ? "0.6" : "1";
toggleBtn.onclick = () => {
titlesVisible = !titlesVisible;
toggleBtn.innerHTML = titlesVisible ? "👁️" : "🙈";
if (titlesVisible) {
toggleBtn.style.opacity = "0.6";
carousel.style.paddingLeft = `${LEFT_WIDTH}px`;
carousel.style.marginLeft = `-${LEFT_WIDTH}px`;
carousel.querySelectorAll('.grok-title-ui').forEach(el => el.style.display = 'flex');
filterInput.style.display = "block";
importBtn.style.display = "inline-block";
exportBtn.style.display = "inline-block";
controls.style.justifyContent = "flex-end";
controls.style.paddingRight = "4px";
} else {
toggleBtn.style.opacity = "1";
carousel.style.paddingLeft = `0px`;
carousel.style.marginLeft = `0px`;
carousel.querySelectorAll('.grok-title-ui').forEach(el => el.style.display = 'none');
filterInput.style.display = "none";
importBtn.style.display = "none";
exportBtn.style.display = "none";
controls.style.justifyContent = "center";
controls.style.paddingRight = "0px";
}
};
controls.appendChild(filterInput);
controls.appendChild(toggleBtn);
controls.appendChild(importBtn);
controls.appendChild(exportBtn);
carousel.insertBefore(controls, carousel.firstChild);
}
const observer = new MutationObserver(() => {
observer.disconnect();
enhance();
observer.observe(document.body, { childList: true, subtree: true });
});
observer.observe(document.body, { childList: true, subtree: true });
enhance();
})();