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