v0.1.5 0b43029

tooltip.svelte RawHistoryFolder

1<script lang="ts">
2  type $$Props = {
3    data: {
4      text: string,
5      direction?: 'top' | 'bottom' | 'left' | 'right'
6    }
7  };
8
9  let { children, data } = $props();
10  
11  // Derive values from data with defaults
12  let text = $derived(data.text);
13  let direction = $derived(data.direction ?? 'top');
14
15  let wrapper: HTMLButtonElement;
16  let tooltipDiv = $state<HTMLDivElement | null>(null);
17  let coords = { x: 0, y: 0 };
18
19  function updateCoords() {
20    if (wrapper) {
21      const rect = wrapper.getBoundingClientRect();
22      if (direction === 'top') {
23        coords = { x: rect.left + rect.width / 2, y: rect.top - 8 };
24      } else if (direction === 'bottom') {
25        coords = { x: rect.left + rect.width / 2, y: rect.bottom + 8 };
26      } else if (direction === 'left') {
27        coords = { x: rect.left - 8, y: rect.top + rect.height / 2 };
28      } else if (direction === 'right') {
29        coords = { x: rect.right + 8, y: rect.top + rect.height / 2 };
30      }
31    }
32  }
33
34  function showTooltip() {
35    updateCoords();
36    if (tooltipDiv) return; // Prevent multiple tooltips
37    tooltipDiv = document.createElement('div');
38    tooltipDiv.className = `tooltip ${direction}`;
39    tooltipDiv.style.position = 'fixed';
40    tooltipDiv.style.zIndex = '1000000';
41    tooltipDiv.textContent = text;
42    tooltipDiv.setAttribute('role', 'tooltip');
43    tooltipDiv.setAttribute('aria-hidden', 'false');
44    document.body.appendChild(tooltipDiv);
45
46    const tooltipRect = tooltipDiv.getBoundingClientRect();
47    let finalLeft = coords.x;
48    let finalTop = coords.y;
49    if (direction === 'top') {
50      finalLeft = coords.x - tooltipRect.width / 2;
51      finalTop = coords.y - tooltipRect.height;
52    } else if (direction === 'bottom') {
53      finalLeft = coords.x - tooltipRect.width / 2;
54      finalTop = coords.y;
55    } else if (direction === 'left') {
56      finalLeft = coords.x - tooltipRect.width;
57      finalTop = coords.y - tooltipRect.height / 2;
58    } else if (direction === 'right') {
59      finalLeft = coords.x;
60      finalTop = coords.y - tooltipRect.height / 2;
61    }
62
63    if (finalLeft < 0) {
64      finalLeft = 0;
65    } else if (finalLeft + tooltipRect.width > window.innerWidth) {
66      finalLeft = window.innerWidth - tooltipRect.width;
67    }
68    if (finalTop < 0) {
69      finalTop = 0;
70    } else if (finalTop + tooltipRect.height > window.innerHeight) {
71      finalTop = window.innerHeight - tooltipRect.height;
72    }
73
74    tooltipDiv.style.left = `${finalLeft}px`;
75    tooltipDiv.style.top = `${finalTop}px`;
76  }
77
78  function hideTooltip() {
79    if (tooltipDiv && tooltipDiv.parentNode) {
80      tooltipDiv.parentNode.removeChild(tooltipDiv);
81      tooltipDiv = null;
82    }
83  }
84
85  function handleKeyDown(e: KeyboardEvent) {
86    if (e.key === 'Escape') {
87      hideTooltip();
88    }
89  }
90</script>
91
92<button 
93  class="tooltip-wrapper" 
94  bind:this={wrapper}
95  onmouseover={showTooltip}
96  onmouseout={hideTooltip}
97  onfocus={showTooltip}
98  onblur={hideTooltip}
99  onkeydown={handleKeyDown}
100  type="button"
101  aria-describedby={tooltipDiv?.id}
102>
103  {@render children()}
104</button>
105
106<style>
107  .tooltip-wrapper {
108    display: inline;
109    width: auto;
110    min-width: 0;
111    background: none !important;
112    border: none !important;
113    padding: 0 !important;
114    margin: 0 !important;
115    font: inherit !important;
116    color: inherit !important;
117    cursor: inherit !important;
118    text-align: inherit !important;
119    box-shadow: none !important;
120    text-shadow: inherit !important;
121    border-radius: 0 !important;
122    -webkit-appearance: none !important;
123    appearance: none !important;
124    white-space: pre;
125  }
126  .tooltip-wrapper:hover, 
127  .tooltip-wrapper:active {
128    background: none !important;
129    border: none !important;
130    box-shadow: none !important;
131  }
132  .tooltip-wrapper:focus {
133    outline: none !important;
134    box-shadow: none !important;
135  }
136  .tooltip-wrapper:focus-visible {
137    outline: 2px solid #003C74 !important;
138    outline-offset: 2px !important;
139  }
140  /* Use :global for styles that need to apply to elements outside the component */
141  :global(.tooltip) {
142    white-space: nowrap;
143    padding: 0.5rem;
144    background: #ECE9D8;
145    color: #000;
146    border: 1px solid #003C74;
147    box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
148    border-radius: 2px;
149    font-size: 0.875rem;
150    font-family: 'Microsoft Sans Serif', 'Segoe UI', Arial, Tahoma, sans-serif;
151  }
152
153  /* Direction-specific styles for the tooltip arrow */
154  :global(.tooltip.top)::before {
155    content: '';
156    position: absolute;
157    bottom: -6px;
158    left: 50%;
159    transform: translateX(-50%);
160    border-left: 6px solid transparent;
161    border-right: 6px solid transparent;
162    border-top: 6px solid #003C74;
163  }
164
165  :global(.tooltip.bottom)::before {
166    content: '';
167    position: absolute;
168    top: -6px;
169    left: 50%;
170    transform: translateX(-50%);
171    border-left: 6px solid transparent;
172    border-right: 6px solid transparent;
173    border-bottom: 6px solid #003C74;
174  }
175
176  :global(.tooltip.left)::before {
177    content: '';
178    position: absolute;
179    right: -6px;
180    top: 50%;
181    transform: translateY(-50%);
182    border-top: 6px solid transparent;
183    border-bottom: 6px solid transparent;
184    border-left: 6px solid #003C74;
185  }
186
187  :global(.tooltip.right)::before {
188    content: '';
189    position: absolute;
190    left: -6px;
191    top: 50%;
192    transform: translateY(-50%);
193    border-top: 6px solid transparent;
194    border-bottom: 6px solid transparent;
195    border-right: 6px solid #003C74;
196  }
197</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 3

Tags 9

↑↓ moveenter openesc close