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 Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
1 file · +30 -9
M src/lib/components/color-picker-256.svelte +30 -9
| @@ -16,6 +16,26 @@ | |||
| 16 | 16 | ||
| 17 | 17 | // State | |
| 18 | 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 | 39 | let gradientColors = $state<string[]>([selectedColor || '#000000']); | |
| 20 | 40 | let activeGradientSlot = $state(0); | |
| 21 | 41 | ||
| @@ -123,6 +143,7 @@ | |||
| 123 | 143 | <button | |
| 124 | 144 | onclick={(e) => { | |
| 125 | 145 | e.stopPropagation(); | |
| 146 | + | if (!showPicker) placePicker(e.currentTarget as HTMLElement); | |
| 126 | 147 | showPicker = !showPicker; | |
| 127 | 148 | }} | |
| 128 | 149 | title={gradientColors.length > 1 ? 'Gradient Color' : 'Text Color'} | |
| @@ -137,7 +158,11 @@ | |||
| 137 | 158 | <!-- Color picker dropdown --> | |
| 138 | 159 | {#if showPicker} | |
| 139 | 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 | 166 | <!-- Gradient controls --> | |
| 142 | 167 | <div class="gradient-controls"> | |
| 143 | 168 | <div class="gradient-slots"> | |
| @@ -233,12 +258,9 @@ | |||
| 233 | 258 | } | |
| 234 | 259 | ||
| 235 | 260 | .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 */ | |
| 240 | 263 | padding: 4px; | |
| 241 | - | margin-top: 2px; | |
| 242 | 264 | background: #ece9d8; /* Authentic XP dialog background */ | |
| 243 | 265 | width: 420px; | |
| 244 | 266 | max-height: 380px; | |
| @@ -251,9 +273,8 @@ | |||
| 251 | 273 | /* Responsive positioning for mobile/small screens */ | |
| 252 | 274 | @media (max-width: 768px) { | |
| 253 | 275 | .picker-dropdown { | |
| 254 | - | position: fixed; | |
| 255 | - | top: 50%; | |
| 256 | - | left: 50%; | |
| 276 | + | top: 50% !important; | |
| 277 | + | left: 50% !important; | |
| 257 | 278 | transform: translate(-50%, -50%); | |
| 258 | 279 | right: auto; | |
| 259 | 280 | width: 90vw; | |
| @@ -16,6 +16,26 @@ | |||||
| 16 | 16 | ||||
| 17 | // State | 17 | // 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 | <button | 143 | <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; | ||