Spaces:
Running
Running
| import gradio as gr | |
| class SidebarMenu(gr.HTML): | |
| def __init__( | |
| self, | |
| menu_data, | |
| value=None, | |
| open=True, | |
| position="left", | |
| width=280, | |
| **kwargs | |
| ): | |
| html_template = """ | |
| <div class="sidebar ${position} ${open ? 'open' : ''}" | |
| style="width:${width}px; ${position}:-${width}px;"> | |
| <button class="toggle-button"> | |
| <i data-lucide="chevron-right" class="toggle-icon"></i> | |
| </button> | |
| <div class="sidebar-content"> | |
| <div class="brand"> | |
| <div class="logo-circle"> | |
| 🌱 | |
| </div> | |
| <span>EVio</span> | |
| </div> | |
| <div class="sidebar-wrapper"> | |
| ${menu_data.map(item => { | |
| if(item.type === "group") { | |
| return ` | |
| <div class="menu-group"> | |
| <div class="group-header"> | |
| <div class="header-left"> | |
| <i | |
| data-lucide="${item.icon}" | |
| class="icon"> | |
| </i> | |
| <span class="label"> | |
| ${item.label} | |
| </span> | |
| </div> | |
| <i | |
| data-lucide="chevron-down" | |
| class="arrow"> | |
| </i> | |
| </div> | |
| <div class="group-children"> | |
| ${item.children.map(child => ` | |
| <div | |
| class="menu-item ${value === child.id ? 'active':''}" | |
| data-id="${child.id}"> | |
| <i | |
| data-lucide="${child.icon}" | |
| class="icon"> | |
| </i> | |
| <span> | |
| ${child.label} | |
| </span> | |
| </div> | |
| `).join("")} | |
| </div> | |
| </div> | |
| ` | |
| } | |
| return ` | |
| <div | |
| class="menu-item ${value === item.id ? 'active':''}" | |
| data-id="${item.id}"> | |
| <i | |
| data-lucide="${item.icon}" | |
| class="icon"> | |
| </i> | |
| <span> | |
| ${item.label} | |
| </span> | |
| </div> | |
| ` | |
| }).join("")} | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| css_template = """ | |
| :root{ | |
| --evio-green:#4CAF50; | |
| --evio-light:#EAF7EC; | |
| --evio-soft:#D8F0DA; | |
| --evio-dark:#245B2A; | |
| --evio-border:#B8DDBB; | |
| } | |
| .sidebar{ | |
| display:flex; | |
| flex-direction:column; | |
| position:fixed; | |
| top:0; | |
| height:100%; | |
| background: | |
| linear-gradient( | |
| 180deg, | |
| #F3FFF4, | |
| #E8F5E9 | |
| ); | |
| border-right: | |
| 1px solid var(--evio-border); | |
| color:var(--evio-dark); | |
| font-family: | |
| Inter, | |
| sans-serif; | |
| z-index:1000; | |
| transition: | |
| transform .35s ease; | |
| box-shadow: | |
| 8px 0 25px rgba(76,175,80,.12); | |
| } | |
| .sidebar.open.left{ | |
| transform: | |
| translateX(100%); | |
| } | |
| .sidebar.open.right{ | |
| transform: | |
| translateX(-100%); | |
| } | |
| /* EVio Logo */ | |
| .brand{ | |
| display:flex; | |
| align-items:center; | |
| gap:12px; | |
| padding: | |
| 25px 20px; | |
| font-size:24px; | |
| font-weight:800; | |
| color: | |
| var(--evio-dark); | |
| } | |
| .logo-circle{ | |
| width:42px; | |
| height:42px; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| background: | |
| white; | |
| border-radius:14px; | |
| box-shadow: | |
| 0 5px 15px rgba(76,175,80,.2); | |
| } | |
| .toggle-button{ | |
| position:absolute; | |
| top:25px; | |
| right:-40px; | |
| width:35px; | |
| height:35px; | |
| border-radius: | |
| 0 12px 12px 0; | |
| border: | |
| 1px solid var(--evio-border); | |
| background:white; | |
| cursor:pointer; | |
| display:flex; | |
| align-items:center; | |
| justify-content:center; | |
| } | |
| .toggle-button:hover{ | |
| background: | |
| var(--evio-light); | |
| } | |
| .toggle-icon{ | |
| color: | |
| var(--evio-green); | |
| width:20px; | |
| height:20px; | |
| } | |
| .sidebar-content{ | |
| overflow-y:auto; | |
| } | |
| .sidebar-wrapper{ | |
| padding: | |
| 10px 15px; | |
| } | |
| .menu-item{ | |
| display:flex; | |
| align-items:center; | |
| gap:12px; | |
| padding: | |
| 14px 16px; | |
| margin: | |
| 7px 0; | |
| border-radius: | |
| 14px; | |
| cursor:pointer; | |
| font-size:15px; | |
| font-weight:600; | |
| color: | |
| var(--evio-dark); | |
| transition:.25s; | |
| } | |
| .menu-item:hover{ | |
| background: | |
| linear-gradient( | |
| 90deg, | |
| #A5D6A7, | |
| #66BB6A | |
| ); | |
| color:white; | |
| transform: | |
| translateX(6px); | |
| box-shadow: | |
| 0 8px 20px rgba(76,175,80,.25); | |
| } | |
| .menu-item.active{ | |
| background: | |
| linear-gradient( | |
| 90deg, | |
| #66BB6A, | |
| #4CAF50 | |
| ); | |
| color:white; | |
| box-shadow: | |
| 0 8px 22px rgba(76,175,80,.35); | |
| } | |
| .group-header{ | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:center; | |
| padding: | |
| 13px 16px; | |
| cursor:pointer; | |
| border-radius:14px; | |
| font-weight:700; | |
| color: | |
| var(--evio-dark); | |
| } | |
| .group-header:hover{ | |
| background: | |
| var(--evio-soft); | |
| } | |
| .header-left{ | |
| display:flex; | |
| align-items:center; | |
| gap:12px; | |
| } | |
| .group-children{ | |
| display:none; | |
| padding-left: | |
| 15px; | |
| } | |
| .icon{ | |
| width:19px; | |
| height:19px; | |
| stroke-width:2.3; | |
| color: | |
| var(--evio-green); | |
| transition:.25s; | |
| } | |
| .menu-item:hover .icon{ | |
| color:white; | |
| transform:scale(1.15); | |
| } | |
| .arrow{ | |
| width:15px; | |
| height:15px; | |
| color: | |
| var(--evio-green); | |
| transition:.3s; | |
| } | |
| @media(max-width:768px){ | |
| .sidebar{ | |
| width:100vw!important; | |
| } | |
| } | |
| """ | |
| js_on_load = """ | |
| let openFolders = new Set(); | |
| function initIcons(){ | |
| if(window.lucide){ | |
| window.lucide.createIcons(); | |
| } | |
| } | |
| function folders(){ | |
| element.querySelectorAll(".group-header") | |
| .forEach(header=>{ | |
| let label = | |
| header.querySelector(".label") | |
| ?.innerText; | |
| let children = | |
| header.nextElementSibling; | |
| let arrow = | |
| header.querySelector(".arrow"); | |
| if(children){ | |
| let open = | |
| openFolders.has(label); | |
| children.style.display = | |
| open ? "block":"none"; | |
| arrow.style.transform = | |
| open ? | |
| "rotate(180deg)" | |
| : | |
| "rotate(0deg)"; | |
| } | |
| }); | |
| } | |
| initIcons(); | |
| folders(); | |
| element.addEventListener( | |
| "click", | |
| e=>{ | |
| if(e.target.closest(".toggle-button")){ | |
| let sidebar = | |
| element.querySelector(".sidebar"); | |
| sidebar.classList.toggle("open"); | |
| return; | |
| } | |
| let header = | |
| e.target.closest(".group-header"); | |
| if(header){ | |
| let label = | |
| header.querySelector(".label") | |
| .innerText; | |
| openFolders.has(label) | |
| ? | |
| openFolders.delete(label) | |
| : | |
| openFolders.add(label); | |
| folders(); | |
| return; | |
| } | |
| let item = | |
| e.target.closest(".menu-item"); | |
| if(item){ | |
| props.value = | |
| item.dataset.id; | |
| trigger("change"); | |
| } | |
| }); | |
| """ | |
| super().__init__( | |
| value=value, | |
| html=html_template, | |
| **kwargs | |
| ) | |
| self.css = css_template | |
| self.js = js_on_load | |
| def api_info(self): | |
| return { | |
| "type":"string" | |
| } |