Sidebar.svelte raw

   1  <script>
   2      export let isDarkTheme = false;
   3      export let tabs = [];
   4      export let selectedTab = "";
   5      export let version = "";
   6      export let mobileOpen = false;
   7  
   8      import { createEventDispatcher } from "svelte";
   9      const dispatch = createEventDispatcher();
  10  
  11      function selectTab(tabId) {
  12          dispatch("selectTab", tabId);
  13          // Close mobile drawer when tab is selected
  14          dispatch("closeMobileMenu");
  15      }
  16  
  17      function closeSearchTab(tabId) {
  18          dispatch("closeSearchTab", tabId);
  19      }
  20  
  21      function closeMobileMenu() {
  22          dispatch("closeMobileMenu");
  23      }
  24  </script>
  25  
  26  <!-- svelte-ignore a11y-click-events-have-key-events -->
  27  <!-- svelte-ignore a11y-no-static-element-interactions -->
  28  {#if mobileOpen}
  29      <div class="mobile-overlay" on:click={closeMobileMenu}></div>
  30  {/if}
  31  
  32  <aside class="sidebar" class:dark-theme={isDarkTheme} class:mobile-open={mobileOpen}>
  33      <div class="sidebar-content">
  34          <div class="tabs">
  35              {#each tabs as tab}
  36                  <button
  37                      class="tab"
  38                      class:active={selectedTab === tab.id}
  39                      on:click={() => selectTab(tab.id)}
  40                  >
  41                      <span class="tab-icon">{tab.icon}</span>
  42                      <span class="tab-label">{tab.label}</span>
  43                      {#if tab.isSearchTab}
  44                          <span
  45                              class="tab-close-icon"
  46                              on:click|stopPropagation={() =>
  47                                  closeSearchTab(tab.id)}
  48                              on:keydown={(e) =>
  49                                  e.key === "Enter" && closeSearchTab(tab.id)}
  50                              role="button"
  51                              tabindex="0">✕</span
  52                          >
  53                      {/if}
  54                  </button>
  55              {/each}
  56          </div>
  57      </div>
  58      {#if version}
  59          <a href="https://next.orly.dev" target="_blank" rel="noopener noreferrer" class="version-link">
  60              <svg class="version-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
  61                  <!-- Mug body -->
  62                  <path d="M5 6h12v2h1.5c1.38 0 2.5 1.12 2.5 2.5v1c0 1.38-1.12 2.5-2.5 2.5H17v1c0 1.66-1.34 3-3 3H8c-1.66 0-3-1.34-3-3V6zm12 6h1.5c.28 0 .5-.22.5-.5v-1c0-.28-.22-.5-.5-.5H17v2z"/>
  63                  <!-- Leaf on mug -->
  64                  <path d="M9 9c1.5 0 3 .5 3 2.5S10.5 14 9 14c0-1.5.5-3.5 2-4.5" stroke="currentColor" stroke-width="1" fill="none"/>
  65              </svg>
  66              <span class="version-text">v{version}</span>
  67          </a>
  68      {/if}
  69  </aside>
  70  
  71  <style>
  72      .sidebar {
  73          position: fixed;
  74          left: 0;
  75          top: 3em;
  76          width: 200px;
  77          bottom: 0;
  78          background: var(--sidebar-bg);
  79          overflow-y: auto;
  80          z-index: 100;
  81      }
  82  
  83      .sidebar-content {
  84          padding: 0;
  85          background: var(--sidebar-bg);
  86      }
  87  
  88      .tabs {
  89          display: flex;
  90          flex-direction: column;
  91          padding: 0;
  92      }
  93  
  94      .tab {
  95          display: flex;
  96          align-items: center;
  97          padding: 0.75em;
  98          padding-left: 1em;
  99          background: transparent;
 100          color: var(--text-color);
 101          border: none;
 102          cursor: pointer;
 103          transition: background-color 0.2s;
 104          gap: 0.75rem;
 105          text-align: left;
 106          width: 100%;
 107      }
 108  
 109      .tab:hover {
 110          background-color: var(--bg-color);
 111      }
 112  
 113      .tab.active {
 114          background-color: var(--bg-color);
 115      }
 116  
 117      .tab-icon {
 118          font-size: 1.2em;
 119          flex-shrink: 0;
 120          width: 1.5em;
 121          text-align: center;
 122      }
 123  
 124      .tab-label {
 125          font-size: 0.9em;
 126          font-weight: 500;
 127          white-space: nowrap;
 128          overflow: hidden;
 129          text-overflow: ellipsis;
 130          flex: 1;
 131      }
 132  
 133      .tab-close-icon {
 134          cursor: pointer;
 135          transition: opacity 0.2s;
 136          font-size: 0.8em;
 137          margin-left: auto;
 138          padding: 0.25rem;
 139          flex-shrink: 0;
 140      }
 141  
 142      .tab-close-icon:hover {
 143          opacity: 0.7;
 144          background-color: var(--warning);
 145          color: var(--text-color);
 146      }
 147  
 148      @media (max-width: 1280px) {
 149          .sidebar {
 150              width: 60px;
 151          }
 152  
 153          .tab-label {
 154              display: none;
 155          }
 156  
 157          .tab-close-icon {
 158              display: none;
 159          }
 160  
 161          .tab {
 162              /* Keep left alignment so icons stay in same position */
 163              justify-content: flex-start;
 164          }
 165      }
 166  
 167      .version-link {
 168          position: absolute;
 169          bottom: 0;
 170          left: 0;
 171          right: 0;
 172          display: flex;
 173          align-items: center;
 174          gap: 0.5rem;
 175          padding: 0.75em 1em;
 176          color: var(--text-color);
 177          text-decoration: none;
 178          font-size: 0.8em;
 179          transition: background-color 0.2s, color 0.2s;
 180          background: transparent;
 181      }
 182  
 183      .version-link:hover {
 184          background-color: var(--bg-color);
 185      }
 186  
 187      .version-icon {
 188          width: 1.2em;
 189          height: 1.2em;
 190          flex-shrink: 0;
 191          color: #4a9c5d;
 192      }
 193  
 194      .version-text {
 195          white-space: nowrap;
 196      }
 197  
 198      @media (max-width: 1280px) {
 199          .version-text {
 200              display: none;
 201          }
 202  
 203          .version-link {
 204              justify-content: flex-start;
 205              padding-left: 1.25em;
 206          }
 207      }
 208  
 209      /* Mobile drawer styles */
 210      .mobile-overlay {
 211          display: none;
 212      }
 213  
 214      @media (max-width: 640px) {
 215          .mobile-overlay {
 216              display: block;
 217              position: fixed;
 218              top: 0;
 219              left: 0;
 220              right: 0;
 221              bottom: 0;
 222              background: rgba(0, 0, 0, 0.5);
 223              z-index: 199;
 224          }
 225  
 226          .sidebar {
 227              transform: translateX(-100%);
 228              transition: transform 0.3s ease;
 229              z-index: 200;
 230              width: 200px;
 231          }
 232  
 233          .sidebar.mobile-open {
 234              transform: translateX(0);
 235          }
 236  
 237          /* Show labels in mobile drawer */
 238          .sidebar.mobile-open .tab-label {
 239              display: block;
 240          }
 241  
 242          .sidebar.mobile-open .tab-close-icon {
 243              display: block;
 244          }
 245  
 246          .sidebar.mobile-open .version-text {
 247              display: inline;
 248          }
 249  
 250          .sidebar.mobile-open .version-link {
 251              padding-left: 1em;
 252          }
 253      }
 254  
 255  </style>
 256