v0.11.0 dc50343

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    --toast-bg: color-mix(in srgb, var(--dynamic-surface) 88%, var(--dynamic-text) 12%);
99    --toast-border: color-mix(in srgb, var(--dynamic-border) 70%, var(--dynamic-text) 30%);
100    --toast-text: var(--dynamic-text);
101    background-color: var(--toast-bg);
102    border: 1px solid var(--toast-border);
103    border-radius: 0;
104    box-shadow: 0 8px 14px rgba(0, 0, 0, 0.08);
105    cursor: pointer;
106    display: inline-flex;
107    margin: 0;
108    pointer-events: auto;
109    padding: 0;
110    transition: transform 0.2s ease, opacity 0.2s ease;
111  }
112
113  .toast-enter {
114    opacity: 1;
115    transform: translateY(0);
116  }
117
118  .toast-exit {
119    opacity: 0;
120    transform: translateY(-16px);
121  }
122
123  .toast-message {
124    color: var(--toast-text);
125    font-size: 0.875rem;
126    font-weight: 600;
127    min-width: 9rem;
128    padding: 0.75rem 1rem;
129    text-align: center;
130  }
131
132  .toast.toast-info {
133    --toast-bg: color-mix(in srgb, var(--dynamic-surface) 82%, var(--dynamic-text) 18%);
134    --toast-border: color-mix(in srgb, var(--dynamic-border) 55%, var(--dynamic-text) 45%);
135  }
136
137  .toast.toast-success {
138    --toast-bg: color-mix(in srgb, var(--dynamic-surface) 78%, var(--dynamic-text) 22%);
139    --toast-border: color-mix(in srgb, var(--dynamic-border) 50%, var(--dynamic-text) 50%);
140  }
141
142  .toast.toast-error {
143    --toast-bg: color-mix(in srgb, var(--dynamic-surface) 74%, var(--dynamic-text) 26%);
144    --toast-border: color-mix(in srgb, var(--dynamic-border) 45%, var(--dynamic-text) 55%);
145    --toast-text: color-mix(in srgb, var(--dynamic-text) 80%, var(--dynamic-muted) 20%);
146  }
147</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