1<script lang="ts">
2 import type { HistoryCleanupRange } from '../types';
3
4 interface Props {
5 onCleanup: (range: HistoryCleanupRange) => void;
6 onClose: () => void;
7 }
8
9 let { onCleanup, onClose }: Props = $props();
10
11 type CleanupOption = {
12 range: HistoryCleanupRange;
13 label: string;
14 shortcut: string;
15 description: string;
16 destructive?: boolean;
17 };
18
19 const options: CleanupOption[] = [
20 { range: 'hour', label: 'LAST HOUR', shortcut: 'D h', description: 'Delete entries from the past 60 minutes' },
21 { range: 'day', label: 'LAST DAY', shortcut: 'D d', description: 'Delete entries from the past 24 hours' },
22 { range: 'week', label: 'LAST WEEK', shortcut: 'D w', description: 'Delete entries from the past 7 days' },
23 { range: 'month', label: 'LAST MONTH', shortcut: 'D m', description: 'Delete entries from the past 30 days' },
24 { range: 'all', label: 'CLEAR ALL', shortcut: 'D D', description: 'Permanently delete all browsing history', destructive: true },
25 ];
26
27 let selectedIndex = $state(0);
28 let confirming = $state(false);
29
30 const handleKeyDown = (e: KeyboardEvent) => {
31 switch (e.key) {
32 case 'Escape':
33 e.preventDefault();
34 if (confirming) {
35 confirming = false;
36 } else {
37 onClose();
38 }
39 break;
40 case 'ArrowDown':
41 case 'j':
42 e.preventDefault();
43 selectedIndex = Math.min(selectedIndex + 1, options.length - 1);
44 break;
45 case 'ArrowUp':
46 case 'k':
47 e.preventDefault();
48 selectedIndex = Math.max(selectedIndex - 1, 0);
49 break;
50 case 'Enter':
51 e.preventDefault();
52 handleSelect(options[selectedIndex]!);
53 break;
54 }
55 };
56
57 const handleSelect = (option: CleanupOption) => {
58 if (option.destructive && !confirming) {
59 confirming = true;
60 return;
61 }
62 onCleanup(option.range);
63 onClose();
64 };
65
66 const handleOverlayClick = (e: Event) => {
67 if (e.target === e.currentTarget) {
68 onClose();
69 }
70 };
71</script>
72
73<svelte:window onkeydown={handleKeyDown} />
74
75<div
76 class="cleanup-overlay"
77 onclick={handleOverlayClick}
78 onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
79 role="button"
80 tabindex="0"
81 aria-label="Close cleanup modal"
82>
83 <div class="cleanup-modal" role="presentation">
84 <div class="modal-header">
85 <span class="modal-icon"></span>
86 <span class="modal-title">CLEAR HISTORY</span>
87 <button class="close-btn" type="button" onclick={onClose}>
88 <kbd>Esc</kbd>
89 </button>
90 </div>
91
92 {#if confirming}
93 <div class="confirm-panel">
94 <div class="confirm-icon"></div>
95 <p class="confirm-message">
96 This will permanently delete <strong>ALL</strong> browsing history.
97 This action cannot be undone.
98 </p>
99 <div class="confirm-actions">
100 <button
101 class="confirm-btn cancel"
102 type="button"
103 onclick={() => confirming = false}
104 >
105 CANCEL
106 <kbd>Esc</kbd>
107 </button>
108 <button
109 class="confirm-btn destructive"
110 type="button"
111 onclick={() => { onCleanup('all'); onClose(); }}
112 >
113 DELETE ALL
114 <kbd>Enter</kbd>
115 </button>
116 </div>
117 </div>
118 {:else}
119 <div class="cleanup-options">
120 {#each options as option, i (option.range)}
121 <button
122 class="cleanup-option"
123 class:selected={selectedIndex === i}
124 class:destructive={option.destructive}
125 type="button"
126 onclick={() => handleSelect(option)}
127 onmouseenter={() => selectedIndex = i}
128 >
129 <div class="option-main">
130 <span class="option-label">{option.label}</span>
131 <span class="option-desc">{option.description}</span>
132 </div>
133 <kbd class="option-shortcut">{option.shortcut}</kbd>
134 </button>
135 {/each}
136 </div>
137 {/if}
138
139 <div class="modal-footer">
140 <span class="hint"><kbd>j</kbd><kbd>k</kbd> navigate</span>
141 <span class="hint"><kbd>Enter</kbd> select</span>
142 </div>
143 </div>
144</div>
145
146<style>
147 .cleanup-overlay {
148 position: fixed;
149 inset: 0;
150 z-index: 100;
151 display: flex;
152 align-items: center;
153 justify-content: center;
154 background: rgb(0 0 0 / 0.75);
155 backdrop-filter: blur(4px);
156 animation: fade-in 150ms ease;
157 }
158
159 @keyframes fade-in {
160 from { opacity: 0; }
161 }
162
163 .cleanup-modal {
164 width: 100%;
165 max-width: 420px;
166 margin: 1rem;
167 background: var(--card);
168 border-width: 1px;
169 border-style: solid;
170 border-color: var(--border);
171 box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6);
172 animation: modal-in 150ms ease;
173 }
174
175 @keyframes modal-in {
176 from {
177 opacity: 0;
178 transform: scale(0.96) translateY(-8px);
179 }
180 }
181
182 .modal-header {
183 display: flex;
184 align-items: center;
185 gap: 0.6rem;
186 padding: 0.75rem 1rem;
187 border-bottom: 1px solid var(--border);
188 background: color-mix(in srgb, var(--background) 80%, transparent);
189 }
190
191 .modal-icon::before {
192 content: '';
193 font-size: 0.9rem;
194 color: var(--warning);
195 }
196
197 .modal-title {
198 flex: 1;
199 font-size: 0.72rem;
200 font-weight: 600;
201 letter-spacing: 0.1em;
202 color: var(--foreground);
203 }
204
205 .close-btn {
206 padding: 0.25rem 0.5rem;
207 background: transparent;
208 border-width: 1px;
209 border-style: solid;
210 border-color: var(--border);
211 cursor: pointer;
212 }
213
214 .close-btn kbd {
215 font-size: 0.6rem;
216 color: var(--muted-foreground);
217 }
218
219 .close-btn:hover {
220 border-color: var(--foreground);
221 }
222
223 .close-btn:hover kbd {
224 color: var(--foreground);
225 }
226
227 .cleanup-options {
228 display: flex;
229 flex-direction: column;
230 }
231
232 .cleanup-option {
233 display: flex;
234 align-items: center;
235 justify-content: space-between;
236 gap: 1rem;
237 padding: 0.85rem 1rem;
238 text-align: left;
239 background: transparent;
240 border: none;
241 border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
242 cursor: pointer;
243 transition: background-color 100ms ease;
244 }
245
246 .cleanup-option:last-child {
247 border-bottom: none;
248 }
249
250 .cleanup-option:hover,
251 .cleanup-option.selected {
252 background: color-mix(in srgb, var(--card) 80%, var(--background) 20%);
253 }
254
255 .cleanup-option.selected {
256 outline: 1px solid var(--primary, #4ade80);
257 outline-offset: -1px;
258 }
259
260 .cleanup-option.destructive .option-label {
261 color: var(--destructive);
262 }
263
264 .option-main {
265 display: flex;
266 flex-direction: column;
267 gap: 0.2rem;
268 }
269
270 .option-label {
271 font-size: 0.75rem;
272 font-weight: 600;
273 letter-spacing: 0.08em;
274 color: var(--foreground);
275 }
276
277 .option-desc {
278 font-size: 0.68rem;
279 color: var(--muted-foreground);
280 letter-spacing: 0.04em;
281 }
282
283 .option-shortcut {
284 padding: 0.2rem 0.45rem;
285 font-size: 0.58rem;
286 letter-spacing: 0.05em;
287 border-width: 1px;
288 border-style: solid;
289 border-color: var(--border);
290 background: var(--background);
291 color: var(--muted-foreground);
292 }
293
294 .confirm-panel {
295 padding: 1.5rem 1rem;
296 display: flex;
297 flex-direction: column;
298 align-items: center;
299 gap: 1rem;
300 text-align: center;
301 }
302
303 .confirm-icon::before {
304 content: '';
305 font-size: 2rem;
306 color: var(--destructive);
307 }
308
309 .confirm-message {
310 margin: 0;
311 font-size: 0.8rem;
312 line-height: 1.5;
313 color: var(--foreground);
314 }
315
316 .confirm-message strong {
317 color: var(--destructive);
318 }
319
320 .confirm-actions {
321 display: flex;
322 gap: 0.5rem;
323 width: 100%;
324 margin-top: 0.5rem;
325 }
326
327 .confirm-btn {
328 flex: 1;
329 display: flex;
330 align-items: center;
331 justify-content: center;
332 gap: 0.5rem;
333 padding: 0.6rem 1rem;
334 font-size: 0.7rem;
335 font-weight: 500;
336 letter-spacing: 0.08em;
337 border-width: 1px;
338 border-style: solid;
339 border-color: var(--border);
340 cursor: pointer;
341 transition: all 100ms ease;
342 }
343
344 .confirm-btn kbd {
345 font-size: 0.55rem;
346 padding: 0.1rem 0.3rem;
347 border: 1px solid;
348 opacity: 0.7;
349 }
350
351 .confirm-btn.cancel {
352 background: transparent;
353 color: var(--muted-foreground);
354 }
355
356 .confirm-btn.cancel:hover {
357 color: var(--foreground);
358 border-color: var(--foreground);
359 }
360
361 .confirm-btn.destructive {
362 background: var(--destructive);
363 color: var(--destructive-foreground);
364 border-color: color-mix(in srgb, var(--destructive) 80%, black 20%);
365 }
366
367 .confirm-btn.destructive:hover {
368 background: color-mix(in srgb, var(--destructive) 85%, white 15%);
369 }
370
371 .modal-footer {
372 display: flex;
373 align-items: center;
374 justify-content: center;
375 gap: 1rem;
376 padding: 0.6rem 1rem;
377 border-top: 1px solid var(--border);
378 background: color-mix(in srgb, var(--background) 60%, transparent);
379 }
380
381 .hint {
382 font-size: 0.6rem;
383 color: var(--muted-foreground);
384 letter-spacing: 0.05em;
385 }
386
387 .hint kbd {
388 font-size: 0.55rem;
389 padding: 0.1rem 0.25rem;
390 margin-right: 0.25rem;
391 border-width: 1px;
392 border-style: solid;
393 border-color: var(--border);
394 background: var(--background);
395 }
396</style>