Skip to content

Instantly share code, notes, and snippets.

@magasine
Last active September 2, 2025 23:24
Show Gist options
  • Select an option

  • Save magasine/e099007d4b8219cb9c23b0828fae5e46 to your computer and use it in GitHub Desktop.

Select an option

Save magasine/e099007d4b8219cb9c23b0828fae5e46 to your computer and use it in GitHub Desktop.
! UI Sidebar Template by @magasine - v20250902 updated
javascript: (function () {
// Constantes e configurações
const CONFIG = {
SIDEBAR_ID: "sidebar-panel-container", // ID do contêiner principal
HEADER_TOGGLE_BTN_ID: "header-toggle-btn",
CONTENT_WRAPPER_ID: "sidebar-content",
HEADER_ID: "sidebar-header",
FOOTER_ID: "sidebar-footer",
MINIMIZE_BTN_ID: "sidebar-minimize-btn",
CLOSE_BTN_ID: "sidebar-close-btn",
RESIZER_ID: "sidebar-resizer",
TITLE_ID: "sidebar-title",
STORAGE_KEYS: {
IS_OPEN: "sidebar-open",
IS_MINIMIZED: "sidebar-minimized",
WIDTH: "sidebar-width",
},
DIMENSIONS: {
MARGIN: 20,
MIN_WIDTH: 200,
TOGGLE_BTN_SIZE: 38,
DEFAULT_WIDTH: "min(300px, 100vw - 30px)",
},
STYLES: {
PRIMARY_COLOR: "#0078D7",
BG_LIGHT: "#fff",
BG_DARK: "#1e1e1e",
TEXT_LIGHT: "#000",
TEXT_DARK: "#f0f0f0",
FONT_FAMILY:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif",
FONT_SIZE: "14px",
LINE_HEIGHT: "1.4",
},
TEXTS: {
TITLE_UI: "Title UI",
FOOTER_UI: "Footer UI",
},
};
// Elementos da UI
let sidebarContainer = null;
let headerToggleBtn = null;
let isMinimized =
localStorage.getItem(CONFIG.STORAGE_KEYS.IS_MINIMIZED) === "true";
// Estado da aplicação
let isResizing = false;
// Utilitários de UI
function createSvgIcon(iconName, size = 24) {
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", size);
svg.setAttribute("height", size);
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("aria-hidden", "true");
svg.style.display = "block";
const path = document.createElementNS(svgNS, "path");
let pathData;
switch (iconName) {
case "toggle-open":
pathData = "M19 12H5M12 5l-7 7 7 7";
break;
case "toggle-close":
pathData = "M5 12h14M12 5l7 7-7 7";
break;
case "close":
pathData = "M6 6l12 12M6 18L18 6";
break;
case "minimize":
pathData = "M12 19V5M5 12l7-7 7 7";
break;
case "maximize":
pathData = "M12 5v14M5 12l7 7 7-7";
break;
default:
pathData = "";
}
path.setAttribute("d", pathData);
path.setAttribute("fill", "none");
path.setAttribute("stroke", "currentColor");
path.setAttribute("stroke-width", "2");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("stroke-linejoin", "round");
svg.appendChild(path);
return svg;
}
function updateHeaderToggleIcon(iconName) {
if (!headerToggleBtn) return;
headerToggleBtn.innerHTML = "";
headerToggleBtn.appendChild(createSvgIcon(iconName, 20));
headerToggleBtn.setAttribute(
"aria-label",
iconName === "toggle-open"
? "Abrir painel lateral"
: "Fechar painel lateral"
);
}
function createHeaderToggleButton() {
const btn = document.createElement("button");
btn.id = CONFIG.HEADER_TOGGLE_BTN_ID;
btn.setAttribute("aria-label", "Abrir painel lateral");
btn.appendChild(createSvgIcon("toggle-open", 20));
Object.assign(btn.style, {
position: "fixed",
top: `${CONFIG.DIMENSIONS.MARGIN}px`,
right: `${CONFIG.DIMENSIONS.MARGIN}px`,
background: CONFIG.STYLES.PRIMARY_COLOR,
border: "none",
color: "#fff",
cursor: "pointer",
padding: "8px",
borderRadius: "4px",
zIndex: "10000",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
transition: "all 0.2s ease",
fontFamily: CONFIG.STYLES.FONT_FAMILY,
fontSize: CONFIG.STYLES.FONT_SIZE,
lineHeight: CONFIG.STYLES.LINE_HEIGHT,
});
btn.onmouseenter = () => {
btn.style.transform = "scale(1.05)";
btn.style.boxShadow = "0 4px 8px rgba(0,0,0,0.3)";
};
btn.onmouseleave = () => {
btn.style.transform = "scale(1)";
btn.style.boxShadow = "0 2px 6px rgba(0,0,0,0.2)";
};
btn.onclick = toggleSidebar;
document.body.appendChild(btn);
return btn;
}
function moveToggleButtonToHeader(shadowRoot) {
const headerControls = shadowRoot.querySelector("#sidebar-header-controls");
if (
headerToggleBtn &&
headerControls &&
!headerControls.contains(headerToggleBtn)
) {
Object.assign(headerToggleBtn.style, {
position: "static",
top: "auto",
right: "auto",
margin: "0",
boxShadow: "none",
});
headerControls.insertBefore(headerToggleBtn, headerControls.firstChild);
updateHeaderToggleIcon("toggle-close");
}
}
function moveToggleButtonToFloating() {
if (!headerToggleBtn) return;
// Remover o botão do header se estiver lá
const sidebarEl = document.getElementById(CONFIG.SIDEBAR_ID);
if (sidebarEl) {
const headerControls = sidebarEl.shadowRoot.querySelector(
"#sidebar-header-controls"
);
if (headerControls && headerControls.contains(headerToggleBtn)) {
headerControls.removeChild(headerToggleBtn);
}
}
// Reposicionar como botão flutuante
Object.assign(headerToggleBtn.style, {
position: "fixed",
top: `${CONFIG.DIMENSIONS.MARGIN}px`,
right: `${CONFIG.DIMENSIONS.MARGIN}px`,
background: CONFIG.STYLES.PRIMARY_COLOR,
boxShadow: "0 2px 6px rgba(0,0,0,0.2)",
});
// Garantir que está no body
if (!document.body.contains(headerToggleBtn)) {
document.body.appendChild(headerToggleBtn);
}
updateHeaderToggleIcon("toggle-open");
}
// Manipulação de redimensionar
function handleResizeStart(e) {
e.preventDefault();
isResizing = true;
document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
}
function handleResizeMove(e) {
if (!isResizing || !sidebarContainer) return;
const newWidth = Math.max(
CONFIG.DIMENSIONS.MIN_WIDTH,
window.innerWidth - e.clientX - CONFIG.DIMENSIONS.MARGIN
);
sidebarContainer.style.width = `${Math.min(
newWidth,
window.innerWidth - CONFIG.DIMENSIONS.MARGIN
)}px`;
}
function handleResizeEnd() {
isResizing = false;
if (sidebarContainer) {
localStorage.setItem(
CONFIG.STORAGE_KEYS.WIDTH,
sidebarContainer.style.width
);
}
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
}
// Controle de estado da sidebar
function toggleMinimizedState() {
isMinimized = !isMinimized;
localStorage.setItem(CONFIG.STORAGE_KEYS.IS_MINIMIZED, isMinimized);
const shadowRoot = sidebarContainer.shadowRoot;
const contentWrapper = shadowRoot.getElementById(CONFIG.CONTENT_WRAPPER_ID);
const footer = shadowRoot.getElementById(CONFIG.FOOTER_ID);
const minimizeBtn = shadowRoot.getElementById(CONFIG.MINIMIZE_BTN_ID);
if (isMinimized) {
contentWrapper.style.display = "none";
footer.style.display = "none";
sidebarContainer.style.height = "fit-content";
minimizeBtn.setAttribute("aria-label", "Restaurar painel");
minimizeBtn.innerHTML = "";
minimizeBtn.appendChild(createSvgIcon("maximize", 20));
} else {
contentWrapper.style.display = "flex";
footer.style.display = "block";
sidebarContainer.style.height = `${
window.innerHeight - CONFIG.DIMENSIONS.MARGIN * 2
}px`;
minimizeBtn.setAttribute("aria-label", "Minimizar painel");
minimizeBtn.innerHTML = "";
minimizeBtn.appendChild(createSvgIcon("minimize", 20));
}
}
function createSidebar() {
if (document.getElementById(CONFIG.SIDEBAR_ID)) {
return;
}
if (!headerToggleBtn) {
headerToggleBtn = createHeaderToggleButton();
}
// 1. Cria o contêiner principal para o Shadow DOM e anexa ao corpo
sidebarContainer = document.createElement("div");
sidebarContainer.id = CONFIG.SIDEBAR_ID;
document.body.appendChild(sidebarContainer);
// 2. Anexa o Shadow Root ao contêiner
const shadowRoot = sidebarContainer.attachShadow({
mode: "open"
});
const savedWidth =
localStorage.getItem(CONFIG.STORAGE_KEYS.WIDTH) ||
CONFIG.DIMENSIONS.DEFAULT_WIDTH;
const sidebarHeight = isMinimized
? "fit-content"
: `${window.innerHeight - CONFIG.DIMENSIONS.MARGIN * 2}px`;
// 3. Adiciona os estilos da sidebar diretamente no Shadow Root
const style = document.createElement("style");
style.textContent = `
:host {
position: fixed;
top: ${CONFIG.DIMENSIONS.MARGIN}px;
right: ${CONFIG.DIMENSIONS.MARGIN}px;
width: ${savedWidth};
height: ${sidebarHeight};
background: var(--sidebar-bg, ${CONFIG.STYLES.BG_LIGHT});
color: var(--sidebar-fg, ${CONFIG.STYLES.TEXT_LIGHT});
box-shadow: -2px 0 12px rgba(0,0,0,0.3);
z-index: 9999999;
display: flex;
flex-direction: column;
transform: translateX(0);
font-family: ${CONFIG.STYLES.FONT_FAMILY};
font-size: ${CONFIG.STYLES.FONT_SIZE};
line-height: ${CONFIG.STYLES.LINE_HEIGHT};
box-sizing: border-box;
transition: transform 0.3s ease-in-out;
}
* {
box-sizing: border-box;
}
#${CONFIG.HEADER_ID} {
background: ${CONFIG.STYLES.PRIMARY_COLOR};
color: #fff;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: default;
}
#${CONFIG.TITLE_ID} {
margin: 0;
padding: 0;
font-size: 18px;
font-weight: 600;
}
#${CONFIG.CONTENT_WRAPPER_ID} {
flex: 1;
overflow: auto;
padding: 16px;
display: ${isMinimized ? "none" : "flex"};
flex-direction: column;
color: inherit;
}
#${CONFIG.FOOTER_ID} {
background: var(--sidebar-bg);
padding: 12px 16px;
text-align: center;
border-top: 1px solid #ddd;
display: ${isMinimized ? "none" : "block"};
}
#${CONFIG.RESIZER_ID} {
position: absolute;
left: 0;
top: 0;
width: 10px;
height: 100%;
cursor: ew-resize;
background: transparent;
}
button {
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
button:hover {
background: rgba(255,255,255,0.2);
}
`;
shadowRoot.appendChild(style);
// Aplicar tema escuro se preferido
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
sidebarContainer.style.setProperty("--sidebar-bg", CONFIG.STYLES.BG_DARK);
sidebarContainer.style.setProperty("--sidebar-fg", CONFIG.STYLES.TEXT_DARK);
}
// 4. Cria os elementos da UI e anexa ao Shadow Root
const header = document.createElement("div");
header.id = CONFIG.HEADER_ID;
const title = document.createElement("h2");
title.id = CONFIG.TITLE_ID;
title.textContent = `${CONFIG.TEXTS.TITLE_UI}`;
header.appendChild(title);
const headerControls = document.createElement("div");
headerControls.id = "sidebar-header-controls";
headerControls.style.display = "flex";
headerControls.style.gap = "8px";
headerControls.style.alignItems = "center";
const minimizeBtn = document.createElement("button");
minimizeBtn.id = CONFIG.MINIMIZE_BTN_ID;
minimizeBtn.setAttribute(
"aria-label",
isMinimized ? "Restaurar painel" : "Minimizar painel"
);
minimizeBtn.appendChild(
createSvgIcon(isMinimized ? "maximize" : "minimize", 20)
);
minimizeBtn.onclick = toggleMinimizedState;
headerControls.appendChild(minimizeBtn);
const closeBtn = document.createElement("button");
closeBtn.id = CONFIG.CLOSE_BTN_ID;
closeBtn.setAttribute("aria-label", "Fechar painel e descarregar toggle");
closeBtn.appendChild(createSvgIcon("close", 20));
closeBtn.onclick = removeSidebarAndToggle;
headerControls.appendChild(closeBtn);
header.appendChild(headerControls);
// Conteúdo principal
const contentWrapper = document.createElement("div");
contentWrapper.id = CONFIG.CONTENT_WRAPPER_ID;
// Constrói o conteúdo de forma programática (seguro para TrustedHTML)
const content = document.createElement("div");
const contentTitle = document.createElement("h3");
contentTitle.textContent = "Content Title";
contentTitle.style.margin = "0 0 12px 0";
const contentParagraph = document.createElement("p");
contentParagraph.textContent = "Side panel content with isolated styles.";
contentParagraph.style.margin = "0 0 12px 0";
const contentList = document.createElement("ul");
contentList.style.margin = "0";
contentList.style.paddingLeft = "20px";
const listItems = ["List item 1", "List item 2", "List item 3"];
listItems.forEach((text, index) => {
const li = document.createElement("li");
li.textContent = text;
li.style.marginBottom = index < listItems.length - 1 ? "8px" : "0";
contentList.appendChild(li);
});
content.appendChild(contentTitle);
content.appendChild(contentParagraph);
content.appendChild(contentList);
contentWrapper.appendChild(content);
// Rodapé
const footer = document.createElement("div");
footer.id = CONFIG.FOOTER_ID;
const footerSmall = document.createElement("small");
footerSmall.textContent = `${CONFIG.TEXTS.FOOTER_UI}`;
footer.appendChild(footerSmall);
// Redimensionador
const resizer = document.createElement("div");
resizer.id = CONFIG.RESIZER_ID;
// Adicionar event listeners
resizer.addEventListener("mousedown", handleResizeStart);
// Montar a sidebar no Shadow Root
shadowRoot.appendChild(header);
shadowRoot.appendChild(contentWrapper);
shadowRoot.appendChild(footer);
shadowRoot.appendChild(resizer);
// Garante que o botão seja movido para o cabeçalho
moveToggleButtonToHeader(shadowRoot);
updateHeaderToggleIcon("toggle-close");
function updateContentHeight() {
if (!isMinimized) {
const headerHeight = header.offsetHeight;
const footerHeight = footer.offsetHeight;
contentWrapper.style.maxHeight = `${
window.innerHeight -
CONFIG.DIMENSIONS.MARGIN * 2 -
headerHeight -
footerHeight
}px`;
} else {
contentWrapper.style.maxHeight = "0px";
}
}
window.addEventListener("resize", updateContentHeight);
localStorage.setItem(CONFIG.STORAGE_KEYS.IS_OPEN, "true");
}
function removeSidebar() {
const sidebarEl = document.getElementById(CONFIG.SIDEBAR_ID);
if (!sidebarEl) return;
// Acessa o Shadow Root para remover os listeners
const shadowRoot = sidebarEl.shadowRoot;
const resizer = shadowRoot.getElementById(CONFIG.RESIZER_ID);
const minimizeBtn = shadowRoot.getElementById(CONFIG.MINIMIZE_BTN_ID);
const closeBtn = shadowRoot.getElementById(CONFIG.CLOSE_BTN_ID);
resizer.removeEventListener("mousedown", handleResizeStart);
minimizeBtn.removeEventListener("click", toggleMinimizedState);
closeBtn.removeEventListener("click", removeSidebarAndToggle);
// Animar e remover a sidebar
sidebarEl.style.transform = "translateX(100%)";
setTimeout(() => sidebarEl.remove(), 300);
localStorage.setItem(CONFIG.STORAGE_KEYS.IS_OPEN, "false");
moveToggleButtonToFloating();
updateHeaderToggleIcon("toggle-open");
}
function removeSidebarAndToggle() {
const sidebarEl = document.getElementById(CONFIG.SIDEBAR_ID);
if (sidebarEl) {
const shadowRoot = sidebarEl.shadowRoot;
const resizer = shadowRoot.getElementById(CONFIG.RESIZER_ID);
const minimizeBtn = shadowRoot.getElementById(CONFIG.MINIMIZE_BTN_ID);
const closeBtn = shadowRoot.getElementById(CONFIG.CLOSE_BTN_ID);
resizer.removeEventListener("mousedown", handleResizeStart);
minimizeBtn.removeEventListener("click", toggleMinimizedState);
closeBtn.removeEventListener("click", removeSidebarAndToggle);
sidebarEl.style.transform = "translateX(100%)";
setTimeout(() => sidebarEl.remove(), 300);
}
if (headerToggleBtn) {
headerToggleBtn.remove();
headerToggleBtn = null;
}
localStorage.setItem(CONFIG.STORAGE_KEYS.IS_OPEN, "false");
}
function toggleSidebar() {
const sidebarEl = document.getElementById(CONFIG.SIDEBAR_ID);
if (sidebarEl) {
removeSidebar();
} else {
createSidebar();
}
}
// A função principal de "loading toggle" do script
function toggleCodeLoad() {
headerToggleBtn = document.getElementById(CONFIG.HEADER_TOGGLE_BTN_ID);
const sidebarEl = document.getElementById(CONFIG.SIDEBAR_ID);
if (sidebarEl) {
removeSidebarAndToggle();
} else {
if (!headerToggleBtn) {
headerToggleBtn = createHeaderToggleButton();
}
createSidebar();
}
}
// Executar a função principal ao carregar o script
toggleCodeLoad();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment