1<script lang="ts">
2 import { homepageState } from '../state.svelte';
3 import type { KeyboardHint } from '../keyboard';
4
5 // Toggle visibility with ?
6 let showHints = $state(false);
7 let hintTimeout: ReturnType<typeof setTimeout> | null = null;
8
9 // Panel-specific hints
10 const panelHints = $derived.by((): KeyboardHint[] => {
11 switch (homepageState.activePanel) {
12 case 'history':
13 return [
14 { keys: ['d', 'x'], description: 'Delete entry' },
15 { keys: ['D h'], description: 'Clear last hour' },
16 { keys: ['D d'], description: 'Clear last day' },
17 { keys: ['D w'], description: 'Clear last week' },
18 { keys: ['D D'], description: 'Clear all history' },
19 { keys: ['D @'], description: 'Clear domain' },
20 { keys: ['p'], description: 'Pin to favorites' },
21 ];
22 case 'favorites':
23 return [
24 { keys: ['e'], description: 'Edit favorite' },
25 { keys: ['t'], description: 'Manage tags' },
26 { keys: ['m'], description: 'Move to folder' },
27 { keys: ['1-9'], description: 'Set shortcut key' },
28 { keys: ['d'], description: 'Delete favorite' },
29 ];
30 case 'analytics':
31 return [
32 { keys: ['r'], description: 'Refresh stats' },
33 { keys: ['D @'], description: 'Clear domain' },
34 ];
35 default:
36 return [];
37 }
38 });
39
40 // Navigation hints (always shown)
41 const navHints: KeyboardHint[] = [
42 { keys: ['j', '↓'], description: 'Move down' },
43 { keys: ['k', '↑'], description: 'Move up' },
44 { keys: ['g g'], description: 'Go to first' },
45 { keys: ['G'], description: 'Go to last' },
46 { keys: ['Enter', 'o'], description: 'Open item' },
47 { keys: ['/'], description: 'Search / Command' },
48 { keys: ['Esc'], description: 'Close / Cancel' },
49 ];
50
51 // Panel switch hints
52 const panelSwitchHints: KeyboardHint[] = [
53 { keys: ['g h'], description: 'History panel' },
54 { keys: ['g f'], description: 'Favorites panel' },
55 { keys: ['g a'], description: 'Analytics panel' },
56 ];
57
58 // Quick access hints
59 const quickAccessHints: KeyboardHint[] = [
60 { keys: ['1-9'], description: 'Quick navigate to shortcut' },
61 ];
62
63 // Handle keyboard toggle
64 function handleKeydown(e: KeyboardEvent) {
65 if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) {
66 // Don't trigger if in input
67 if ((e.target as HTMLElement).tagName === 'INPUT') return;
68 // Don't trigger if command palette is open
69 if (homepageState.commandPaletteOpen) return;
70
71 e.preventDefault();
72 toggleHints();
73 }
74 }
75
76 function toggleHints() {
77 showHints = !showHints;
78
79 // Auto-hide after 10 seconds
80 if (showHints) {
81 if (hintTimeout) clearTimeout(hintTimeout);
82 hintTimeout = setTimeout(() => {
83 showHints = false;
84 }, 10000);
85 } else {
86 if (hintTimeout) {
87 clearTimeout(hintTimeout);
88 hintTimeout = null;
89 }
90 }
91 }
92
93 function closeHints() {
94 showHints = false;
95 if (hintTimeout) {
96 clearTimeout(hintTimeout);
97 hintTimeout = null;
98 }
99 }
100
101 // Cleanup on destroy
102 import { onDestroy } from 'svelte';
103 onDestroy(() => {
104 if (hintTimeout) clearTimeout(hintTimeout);
105 });
106</script>
107
108<svelte:document onkeydown={handleKeydown} />
109
110{#if showHints}
111 <div
112 class="hints-overlay"
113 onclick={closeHints}
114 onkeydown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') closeHints(); }}
115 role="button"
116 tabindex="0"
117 aria-label="Close keyboard hints"
118 >
119 <div class="hints-panel" onclick={(e) => e.stopPropagation()} role="presentation">
120 <div class="hints-header">
121 <span class="hints-icon"></span>
122 <span class="hints-title">KEYBOARD SHORTCUTS</span>
123 <button class="hints-close" onclick={closeHints} type="button">
124 <span>×</span>
125 </button>
126 </div>
127
128 <div class="hints-body">
129 <!-- Navigation Section -->
130 <section class="hints-section">
131 <h3 class="section-title">
132 <span class="section-icon"></span>
133 Navigation
134 </h3>
135 <div class="hints-grid">
136 {#each navHints as hint (hint.description)}
137 <div class="hint-row">
138 <div class="hint-keys">
139 {#each hint.keys as key, i}
140 {#if i > 0}<span class="key-sep">/</span>{/if}
141 <kbd>{key}</kbd>
142 {/each}
143 </div>
144 <span class="hint-desc">{hint.description}</span>
145 </div>
146 {/each}
147 </div>
148 </section>
149
150 <!-- Panel Switch Section -->
151 <section class="hints-section">
152 <h3 class="section-title">
153 <span class="section-icon"></span>
154 Panels
155 </h3>
156 <div class="hints-grid">
157 {#each panelSwitchHints as hint (hint.description)}
158 <div class="hint-row">
159 <div class="hint-keys">
160 {#each hint.keys as key, i}
161 {#if i > 0}<span class="key-sep">/</span>{/if}
162 <kbd>{key}</kbd>
163 {/each}
164 </div>
165 <span class="hint-desc">{hint.description}</span>
166 </div>
167 {/each}
168 </div>
169 </section>
170
171 <!-- Current Panel Section -->
172 {#if panelHints.length > 0}
173 <section class="hints-section panel-specific">
174 <h3 class="section-title">
175 <span class="section-icon"></span>
176 {homepageState.activePanel.charAt(0).toUpperCase() + homepageState.activePanel.slice(1)} Actions
177 </h3>
178 <div class="hints-grid">
179 {#each panelHints as hint (hint.description)}
180 <div class="hint-row">
181 <div class="hint-keys">
182 {#each hint.keys as key, i}
183 {#if i > 0}<span class="key-sep">/</span>{/if}
184 <kbd>{key}</kbd>
185 {/each}
186 </div>
187 <span class="hint-desc">{hint.description}</span>
188 </div>
189 {/each}
190 </div>
191 </section>
192 {/if}
193
194 <!-- Quick Access Section -->
195 <section class="hints-section">
196 <h3 class="section-title">
197 <span class="section-icon"></span>
198 Quick Access
199 </h3>
200 <div class="hints-grid">
201 {#each quickAccessHints as hint (hint.description)}
202 <div class="hint-row">
203 <div class="hint-keys">
204 {#each hint.keys as key, i}
205 {#if i > 0}<span class="key-sep">/</span>{/if}
206 <kbd>{key}</kbd>
207 {/each}
208 </div>
209 <span class="hint-desc">{hint.description}</span>
210 </div>
211 {/each}
212 </div>
213 </section>
214 </div>
215
216 <div class="hints-footer">
217 <span class="footer-hint">Press <kbd>?</kbd> to toggle hints</span>
218 </div>
219 </div>
220 </div>
221{/if}
222
223<style>
224 .hints-overlay {
225 position: fixed;
226 inset: 0;
227 z-index: 150;
228 display: flex;
229 align-items: center;
230 justify-content: center;
231 background: rgb(0 0 0 / 0.6);
232 backdrop-filter: blur(4px);
233 animation: fade-in 150ms ease;
234 }
235
236 @keyframes fade-in {
237 from { opacity: 0; }
238 }
239
240 .hints-panel {
241 width: 100%;
242 max-width: 540px;
243 max-height: 85vh;
244 margin: 1rem;
245 display: flex;
246 flex-direction: column;
247 background: var(--card);
248 border-width: 1px;
249 border-style: solid;
250 border-color: var(--border);
251 box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6);
252 animation: panel-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
253 overflow: hidden;
254 }
255
256 @keyframes panel-in {
257 from {
258 opacity: 0;
259 transform: scale(0.95);
260 }
261 }
262
263 /* Header */
264 .hints-header {
265 display: flex;
266 align-items: center;
267 gap: 0.6rem;
268 padding: 0.85rem 1rem;
269 border-bottom: 1px solid var(--border);
270 background: color-mix(in srgb, var(--background) 80%, transparent);
271 }
272
273 .hints-icon {
274 font-size: 1rem;
275 color: var(--primary, #4ade80);
276 }
277
278 .hints-title {
279 flex: 1;
280 font-size: 0.75rem;
281 font-weight: 600;
282 letter-spacing: 0.1em;
283 color: var(--foreground);
284 }
285
286 .hints-close {
287 display: flex;
288 align-items: center;
289 justify-content: center;
290 width: 1.75rem;
291 height: 1.75rem;
292 font-size: 1.25rem;
293 color: var(--muted-foreground);
294 background: transparent;
295 border-width: 1px;
296 border-style: solid;
297 border-color: var(--border);
298 cursor: pointer;
299 transition: all 120ms ease;
300 }
301
302 .hints-close:hover {
303 color: var(--foreground);
304 border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%);
305 }
306
307 /* Body */
308 .hints-body {
309 flex: 1;
310 overflow-y: auto;
311 padding: 1rem;
312 display: flex;
313 flex-direction: column;
314 gap: 1.25rem;
315 }
316
317 .hints-section {
318 display: flex;
319 flex-direction: column;
320 gap: 0.6rem;
321 }
322
323 .hints-section.panel-specific {
324 padding: 0.75rem;
325 background: color-mix(in srgb, var(--primary, #4ade80) 8%, transparent);
326 border: 1px solid color-mix(in srgb, var(--primary, #4ade80) 25%, var(--border) 75%);
327 }
328
329 .section-title {
330 display: flex;
331 align-items: center;
332 gap: 0.5rem;
333 margin: 0;
334 font-size: 0.72rem;
335 font-weight: 600;
336 text-transform: uppercase;
337 letter-spacing: 0.1em;
338 color: var(--foreground);
339 }
340
341 .section-icon {
342 font-size: 0.85rem;
343 color: var(--muted-foreground);
344 }
345
346 .panel-specific .section-icon {
347 color: var(--primary, #4ade80);
348 }
349
350 .hints-grid {
351 display: grid;
352 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
353 gap: 0.4rem;
354 }
355
356 .hint-row {
357 display: flex;
358 align-items: center;
359 gap: 0.6rem;
360 padding: 0.4rem 0.6rem;
361 background: color-mix(in srgb, var(--background) 60%, transparent);
362 border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
363 }
364
365 .hint-keys {
366 display: flex;
367 align-items: center;
368 gap: 0.2rem;
369 flex-shrink: 0;
370 }
371
372 .hint-keys kbd {
373 padding: 0.2rem 0.4rem;
374 font-size: 0.68rem;
375 font-family: inherit;
376 color: var(--foreground);
377 background: var(--background);
378 border-width: 1px;
379 border-style: solid;
380 border-color: var(--border);
381 min-width: 1.4rem;
382 text-align: center;
383 }
384
385 .key-sep {
386 font-size: 0.6rem;
387 color: var(--muted-foreground);
388 opacity: 0.6;
389 }
390
391 .hint-desc {
392 font-size: 0.72rem;
393 color: var(--muted-foreground);
394 white-space: nowrap;
395 overflow: hidden;
396 text-overflow: ellipsis;
397 }
398
399 /* Footer */
400 .hints-footer {
401 display: flex;
402 justify-content: center;
403 padding: 0.65rem 1rem;
404 border-top: 1px solid var(--border);
405 background: color-mix(in srgb, var(--background) 70%, transparent);
406 }
407
408 .footer-hint {
409 font-size: 0.68rem;
410 color: var(--muted-foreground);
411 letter-spacing: 0.05em;
412 }
413
414 .footer-hint kbd {
415 padding: 0.15rem 0.35rem;
416 font-size: 0.62rem;
417 background: var(--background);
418 border-width: 1px;
419 border-style: solid;
420 border-color: var(--border);
421 margin: 0 0.25rem;
422 }
423
424 /* Scrollbar */
425 .hints-body::-webkit-scrollbar {
426 width: 6px;
427 }
428
429 .hints-body::-webkit-scrollbar-track {
430 background: transparent;
431 }
432
433 .hints-body::-webkit-scrollbar-thumb {
434 background: var(--border);
435 }
436
437 /* Responsive */
438 @media (max-width: 480px) {
439 .hints-grid {
440 grid-template-columns: 1fr;
441 }
442
443 .hints-panel {
444 max-height: 90vh;
445 }
446 }
447</style>