detached e948ac3

fix(color-picker): render dropdown fixed above windows so it is not clipped

commit
e948ac396d0fcdec2d0b74a078decacca95777b5
author
Kit · · 2026-09-27 14:08 UTC
parent
d89cabf
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
1 file · +30 -9
M src/lib/components/color-picker-256.svelte +30 -9
@@ -16,6 +16,26 @@
1616
1717 // State
1818 let showPicker = $state(false);
19+ // Fixed viewport position so the dropdown escapes the window's overflow clipping
20+ let pickerPos = $state({ left: 0, top: 0 });
21+ const PICKER_WIDTH = 420;
22+ const PICKER_MAX_HEIGHT = 380;
23+
24+ function placePicker(button: HTMLElement) {
25+ const rect = button.getBoundingClientRect();
26+ const margin = 4;
27+ const left = Math.min(
28+ Math.max(margin, rect.right - PICKER_WIDTH),
29+ window.innerWidth - PICKER_WIDTH - margin
30+ );
31+ // Prefer opening upward (the toolbar sits above the input at the bottom)
32+ const above = rect.top - PICKER_MAX_HEIGHT - 2;
33+ const top =
34+ above >= margin
35+ ? above
36+ : Math.min(rect.bottom + 2, window.innerHeight - PICKER_MAX_HEIGHT - margin);
37+ pickerPos = { left: Math.max(margin, left), top: Math.max(margin, top) };
38+ }
1939 let gradientColors = $state<string[]>([selectedColor || '#000000']);
2040 let activeGradientSlot = $state(0);
2141
@@ -123,6 +143,7 @@
123143 <button
124144 onclick={(e) => {
125145 e.stopPropagation();
146+ if (!showPicker) placePicker(e.currentTarget as HTMLElement);
126147 showPicker = !showPicker;
127148 }}
128149 title={gradientColors.length > 1 ? 'Gradient Color' : 'Text Color'}
@@ -137,7 +158,11 @@
137158 <!-- Color picker dropdown -->
138159 {#if showPicker}
139160 <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
140- <div class="picker-dropdown sunken-panel" onclick={(e) => e.stopPropagation()}>
161+ <div
162+ class="picker-dropdown sunken-panel"
163+ style="left: {pickerPos.left}px; top: {pickerPos.top}px;"
164+ onclick={(e) => e.stopPropagation()}
165+ >
141166 <!-- Gradient controls -->
142167 <div class="gradient-controls">
143168 <div class="gradient-slots">
@@ -233,12 +258,9 @@
233258 }
234259
235260 .picker-dropdown {
236- position: absolute;
237- top: 100%;
238- right: 0; /* Changed from left: 0 to prevent cutoff */
239- z-index: 1000;
261+ position: fixed;
262+ z-index: 6000; /* above windows and the taskbar */
240263 padding: 4px;
241- margin-top: 2px;
242264 background: #ece9d8; /* Authentic XP dialog background */
243265 width: 420px;
244266 max-height: 380px;
@@ -251,9 +273,8 @@
251273 /* Responsive positioning for mobile/small screens */
252274 @media (max-width: 768px) {
253275 .picker-dropdown {
254- position: fixed;
255- top: 50%;
256- left: 50%;
276+ top: 50% !important;
277+ left: 50% !important;
257278 transform: translate(-50%, -50%);
258279 right: auto;
259280 width: 90vw;
@@ -16,6 +16,26 @@
16 16
17 // State17 // State
18 let showPicker = $state(false);18 let showPicker = $state(false);
19+ // Fixed viewport position so the dropdown escapes the window's overflow clipping
20+ let pickerPos = $state({ left: 0, top: 0 });
21+ const PICKER_WIDTH = 420;
22+ const PICKER_MAX_HEIGHT = 380;
23+
24+ function placePicker(button: HTMLElement) {
25+ const rect = button.getBoundingClientRect();
26+ const margin = 4;
27+ const left = Math.min(
28+ Math.max(margin, rect.right - PICKER_WIDTH),
29+ window.innerWidth - PICKER_WIDTH - margin
30+ );
31+ // Prefer opening upward (the toolbar sits above the input at the bottom)
32+ const above = rect.top - PICKER_MAX_HEIGHT - 2;
33+ const top =
34+ above >= margin
35+ ? above
36+ : Math.min(rect.bottom + 2, window.innerHeight - PICKER_MAX_HEIGHT - margin);
37+ pickerPos = { left: Math.max(margin, left), top: Math.max(margin, top) };
38+ }
19 let gradientColors = $state<string[]>([selectedColor || '#000000']);39 let gradientColors = $state<string[]>([selectedColor || '#000000']);
20 let activeGradientSlot = $state(0);40 let activeGradientSlot = $state(0);
21 41
@@ -123,6 +143,7 @@
123 <button143 <button
124 onclick={(e) => {144 onclick={(e) => {
125 e.stopPropagation();145 e.stopPropagation();
146+ if (!showPicker) placePicker(e.currentTarget as HTMLElement);
126 showPicker = !showPicker;147 showPicker = !showPicker;
127 }}148 }}
128 title={gradientColors.length > 1 ? 'Gradient Color' : 'Text Color'}149 title={gradientColors.length > 1 ? 'Gradient Color' : 'Text Color'}
@@ -137,7 +158,11 @@
137 <!-- Color picker dropdown -->158 <!-- Color picker dropdown -->
138 {#if showPicker}159 {#if showPicker}
139 <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->160 <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
140- <div class="picker-dropdown sunken-panel" onclick={(e) => e.stopPropagation()}>161+ <div
162+ class="picker-dropdown sunken-panel"
163+ style="left: {pickerPos.left}px; top: {pickerPos.top}px;"
164+ onclick={(e) => e.stopPropagation()}
165+ >
141 <!-- Gradient controls -->166 <!-- Gradient controls -->
142 <div class="gradient-controls">167 <div class="gradient-controls">
143 <div class="gradient-slots">168 <div class="gradient-slots">
@@ -233,12 +258,9 @@
233 }258 }
234 259
235 .picker-dropdown {260 .picker-dropdown {
236- position: absolute;261+ position: fixed;
237- top: 100%;262+ z-index: 6000; /* above windows and the taskbar */
238- right: 0; /* Changed from left: 0 to prevent cutoff */
239- z-index: 1000;
240 padding: 4px;263 padding: 4px;
241- margin-top: 2px;
242 background: #ece9d8; /* Authentic XP dialog background */264 background: #ece9d8; /* Authentic XP dialog background */
243 width: 420px;265 width: 420px;
244 max-height: 380px;266 max-height: 380px;
@@ -251,9 +273,8 @@
251 /* Responsive positioning for mobile/small screens */273 /* Responsive positioning for mobile/small screens */
252 @media (max-width: 768px) {274 @media (max-width: 768px) {
253 .picker-dropdown {275 .picker-dropdown {
254- position: fixed;276+ top: 50% !important;
255- top: 50%;277+ left: 50% !important;
256- left: 50%;
257 transform: translate(-50%, -50%);278 transform: translate(-50%, -50%);
258 right: auto;279 right: auto;
259 width: 90vw;280 width: 90vw;

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 8

↑↓ moveenter openesc close