v0.10.0 0f4e9eb

Toast.svelte RawHistoryFolder

1<script lang="ts">
2  import { onMount, onDestroy } from 'svelte';
3
4  interface ToastProps {
5    id: number;
6    message: string;
7    duration?: number;
8    type?: 'info' | 'success' | 'error';
9    onDismiss: (id: number) => void;
10  }
11
12  let { id, message, duration = 2500, type = 'info', onDismiss }: ToastProps = $props();
13
14  let isExiting = $state(false);
15  let isDarkMode = $state(false);
16  let themeObserver: MutationObserver | undefined;
17  let timeoutId: number | undefined;
18
19  onMount(() => {
20    timeoutId = window.setTimeout(() => {
21      dismiss();
22    }, duration);
23
24    syncTheme();
25
26    if (typeof window !== 'undefined' && 'MutationObserver' in window) {
27      themeObserver = new MutationObserver(syncTheme);
28      themeObserver.observe(document.documentElement, {
29        attributes: true,
30        attributeFilter: ['class']
31      });
32    }
33  });
34
35  onDestroy(() => {
36    if (timeoutId) {
37      window.clearTimeout(timeoutId);
38      timeoutId = undefined;
39    }
40
41    if (themeObserver) {
42      themeObserver.disconnect();
43      themeObserver = undefined;
44    }
45  });
46
47  function dismiss() {
48    if (timeoutId) {
49      window.clearTimeout(timeoutId);
50      timeoutId = undefined;
51    }
52    isExiting = true;
53    // Allow time for exit animation before removing from parent
54    setTimeout(() => {
55      onDismiss(id);
56    }, 250);
57  }
58
59  function handleClick() {
60    dismiss();
61  }
62
63  function syncTheme() {
64    if (typeof document === 'undefined') {
65      return;
66    }
67
68    const prefersDark = document.documentElement.classList.contains('dark');
69
70    if (prefersDark) {
71      isDarkMode = true;
72      return;
73    }
74
75    try {
76      isDarkMode = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
77    } catch {
78      isDarkMode = false;
79    }
80  }
81</script>
82
83<button
84  class={`toast ${isDarkMode ? 'toast-dark' : 'toast-light'} toast-${type} ${isExiting ? 'toast-exit' : 'toast-enter'}`}
85  onclick={handleClick}
86  data-toast-id={id}
87  type="button"
88  aria-label="Dismiss notification"
89  title="Click to dismiss"
90>
91  <div class="toast-message">
92    {message || 'Notification'}
93  </div>
94</button>
95
96<style>
97  .toast {
98    background-color: var(--dynamic-surface);
99    border: 1px solid var(--dynamic-border);
100    border-radius: 0;
101    box-shadow: 0 8px 12px rgba(0, 0, 0, 0.08);
102    cursor: pointer;
103    display: inline-flex;
104    margin: 0;
105    pointer-events: auto;
106    padding: 0;
107    transition: transform 0.2s ease, opacity 0.2s ease;
108  }
109
110  .toast-enter {
111    opacity: 1;
112    transform: translateY(0);
113  }
114
115  .toast-exit {
116    opacity: 0;
117    transform: translateY(-16px);
118  }
119
120  .toast-message {
121    color: var(--dynamic-text);
122    font-size: 0.875rem;
123    font-weight: 600;
124    min-width: 9rem;
125    padding: 0.75rem 1rem;
126    text-align: center;
127  }
128
129  .toast-light.toast-info {
130    background-color: rgb(239 246 255);
131    border-color: rgb(191 219 254);
132  }
133
134  .toast-dark.toast-info {
135    background-color: rgb(59 130 246 / 0.2);
136    border-color: rgb(59 130 246);
137  }
138
139  .toast-light.toast-success {
140    background-color: rgb(240 253 244);
141    border-color: rgb(187 247 208);
142  }
143
144  .toast-dark.toast-success {
145    background-color: rgb(22 163 74 / 0.2);
146    border-color: rgb(22 163 74);
147  }
148
149  .toast-light.toast-error {
150    background-color: rgb(254 242 242);
151    border-color: rgb(254 202 202);
152  }
153
154  .toast-dark.toast-error {
155    background-color: rgb(220 38 38 / 0.2);
156    border-color: rgb(220 38 38);
157  }
158
159  .toast-light.toast-info .toast-message {
160    color: rgb(59 130 246);
161  }
162
163  .toast-dark.toast-info .toast-message {
164    color: rgb(191 219 254);
165  }
166
167  .toast-light.toast-success .toast-message {
168    color: rgb(22 163 74);
169  }
170
171  .toast-dark.toast-success .toast-message {
172    color: rgb(187 247 208);
173  }
174
175  .toast-light.toast-error .toast-message {
176    color: rgb(220 38 38);
177  }
178
179  .toast-dark.toast-error .toast-message {
180    color: rgb(254 202 202);
181  }
182</style>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close