Instantly share code, notes, and snippets.
Last active
September 2, 2025 23:24
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save magasine/e099007d4b8219cb9c23b0828fae5e46 to your computer and use it in GitHub Desktop.
! UI Sidebar Template by @magasine - v20250902 updated
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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