color-picker-256.svelte RawHistoryFolder
1<script lang="ts">
2 import { generate256Colors, CLASSIC_COLORS } from '../types/text-formatting';
3
4 // Props
5 let {
6 selectedColor = $bindable('#000000'),
7 selectedGradient = $bindable(undefined),
8 onColorChange,
9 onGradientChange
10 } = $props<{
11 selectedColor?: string;
12 selectedGradient?: string[];
13 onColorChange?: (color: string) => void;
14 onGradientChange?: (gradient: string[]) => void;
15 }>();
16
17 // State
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 }
39 let gradientColors = $state<string[]>([selectedColor || '#000000']);
40 let activeGradientSlot = $state(0);
41
42 // Generate color palette
43 const colors256 = generate256Colors();
44 const classicColors = Object.values(CLASSIC_COLORS);
45
46 // Initialize gradient colors from props
47 $effect(() => {
48 if (selectedGradient && selectedGradient.length > 0) {
49 gradientColors = [...selectedGradient];
50 } else if (selectedColor) {
51 gradientColors = [selectedColor];
52 }
53 });
54
55 // Get current display color/gradient
56 let displayStyle = $derived(() => {
57 if (gradientColors.length > 1) {
58 return `background: linear-gradient(to right, ${gradientColors.join(', ')})`;
59 }
60 return `background: ${gradientColors[0] || '#000000'}`;
61 });
62
63 // Handle color selection
64 function selectColor(color: string) {
65 if (activeGradientSlot < gradientColors.length) {
66 gradientColors[activeGradientSlot] = color;
67 gradientColors = [...gradientColors]; // Trigger reactivity
68
69 if (gradientColors.length === 1) {
70 // Single color mode
71 selectedColor = color;
72 selectedGradient = undefined;
73 onColorChange?.(color);
74 } else {
75 // Gradient mode
76 selectedGradient = [...gradientColors];
77 selectedColor = gradientColors[0];
78 onGradientChange?.(gradientColors);
79 }
80 }
81 }
82
83 // Gradient management
84 function addGradientColor() {
85 if (gradientColors.length < 3) {
86 const newColor = gradientColors[gradientColors.length - 1] || '#FFFFFF';
87 gradientColors = [...gradientColors, newColor];
88 activeGradientSlot = gradientColors.length - 1;
89 selectedGradient = [...gradientColors];
90 onGradientChange?.(gradientColors);
91 }
92 }
93
94 function removeGradientColor(index: number) {
95 if (gradientColors.length > 1) {
96 gradientColors = gradientColors.filter((_, i) => i !== index);
97 activeGradientSlot = Math.min(activeGradientSlot, gradientColors.length - 1);
98
99 if (gradientColors.length === 1) {
100 // Back to single color mode
101 selectedColor = gradientColors[0];
102 selectedGradient = undefined;
103 onColorChange?.(gradientColors[0]);
104 } else {
105 selectedGradient = [...gradientColors];
106 onGradientChange?.(gradientColors);
107 }
108 }
109 }
110
111 function setActiveSlot(index: number) {
112 activeGradientSlot = index;
113 }
114
115 // Close picker
116 function closePicker() {
117 showPicker = false;
118 }
119
120 // Handle click outside to close
121 function handleClickOutside(event: MouseEvent) {
122 const target = event.target as HTMLElement;
123 const colorPickerContainer = document.querySelector('.color-picker-container');
124
125 if (showPicker && colorPickerContainer && !colorPickerContainer.contains(target)) {
126 closePicker();
127 }
128 }
129
130 // Add/remove click outside listener
131 $effect(() => {
132 if (showPicker) {
133 document.addEventListener('click', handleClickOutside);
134 return () => {
135 document.removeEventListener('click', handleClickOutside);
136 };
137 }
138 });
139</script>
140
141<div class="color-picker-container">
142 <!-- Color picker button -->
143 <button
144 onclick={(e) => {
145 e.stopPropagation();
146 if (!showPicker) placePicker(e.currentTarget as HTMLElement);
147 showPicker = !showPicker;
148 }}
149 title={gradientColors.length > 1 ? 'Gradient Color' : 'Text Color'}
150 style="width: 20px; height: 20px; padding: 0;"
151 >
152 <div
153 class="color-indicator"
154 style="{displayStyle()}; width: 12px; height: 12px; border: 1px solid #000;"
155 ></div>
156 </button>
157
158 <!-- Color picker dropdown -->
159 {#if showPicker}
160 <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
161 <div
162 class="picker-dropdown sunken-panel"
163 style="left: {pickerPos.left}px; top: {pickerPos.top}px;"
164 onclick={(e) => e.stopPropagation()}
165 >
166 <!-- Gradient controls -->
167 <div class="gradient-controls">
168 <div class="gradient-slots">
169 {#each gradientColors as color, index (index)}
170 <button
171 class="gradient-slot"
172 class:active={activeGradientSlot === index}
173 style="background-color: {color} !important;"
174 onclick={() => setActiveSlot(index)}
175 title="Color {index + 1} - Click to select this color slot"
176 >
177 {#if gradientColors.length > 1}
178 <span
179 class="remove-slot"
180 onclick={(e) => {
181 e.stopPropagation();
182 removeGradientColor(index);
183 }}
184 onkeydown={(e) => {
185 if (e.key === 'Enter' || e.key === ' ') {
186 e.preventDefault();
187 e.stopPropagation();
188 removeGradientColor(index);
189 }
190 }}
191 title="Remove this color"
192 role="button"
193 tabindex="0">×</span
194 >
195 {/if}
196 </button>
197 {/each}
198
199 {#if gradientColors.length < 3}
200 <button
201 class="add-slot"
202 onclick={addGradientColor}
203 title="Add another color to create gradient">+</button
204 >
205 {/if}
206 </div>
207
208 {#if gradientColors.length > 1}
209 <div
210 class="gradient-preview"
211 style="background: linear-gradient(90deg, {gradientColors.join(', ')});"
212 ></div>
213 {/if}
214 </div>
215
216 <!-- Classic colors (first row) -->
217 <div class="color-section">
218 <div class="section-title">Classic Colors</div>
219 <div class="color-row">
220 {#each classicColors as color (color)}
221 <button
222 class="color-swatch classic-swatch"
223 style="--swatch-color: {color}; background-color: {color} !important;"
224 onclick={() => selectColor(color)}
225 title={color}
226 ></button>
227 {/each}
228 </div>
229 </div>
230
231 <!-- 256-color palette -->
232 <div class="color-section">
233 <div class="section-title">Full Palette</div>
234 <div class="color-grid">
235 {#each colors256 as color (color)}
236 <button
237 class="color-swatch"
238 style="--swatch-color: {color}; background-color: {color} !important;"
239 onclick={() => selectColor(color)}
240 title={color}
241 ></button>
242 {/each}
243 </div>
244 </div>
245
246 <!-- Close button -->
247 <div class="picker-footer">
248 <button onclick={closePicker}>Close</button>
249 </div>
250 </div>
251 {/if}
252</div>
253
254<style>
255 .color-picker-container {
256 position: relative;
257 display: inline-block;
258 }
259
260 .picker-dropdown {
261 position: fixed;
262 z-index: 6000; /* above windows and the taskbar */
263 padding: 4px;
264 background: #ece9d8; /* Authentic XP dialog background */
265 width: 420px;
266 max-height: 380px;
267 overflow-y: auto;
268 border: 2px outset #c0c0c0;
269 box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
270 box-sizing: border-box;
271 }
272
273 /* Responsive positioning for mobile/small screens */
274 @media (max-width: 768px) {
275 .picker-dropdown {
276 top: 50% !important;
277 left: 50% !important;
278 transform: translate(-50%, -50%);
279 right: auto;
280 width: 90vw;
281 max-width: 420px;
282 max-height: 85vh;
283 }
284 }
285
286 .gradient-controls {
287 margin-bottom: 6px;
288 padding: 4px;
289 border: 1px inset #dfdfdf;
290 background: #ece9d8;
291 }
292
293 .gradient-slots {
294 display: flex;
295 gap: 4px;
296 margin-bottom: 4px;
297 align-items: center;
298 flex-wrap: wrap;
299 }
300
301 .gradient-slot {
302 width: 24px;
303 height: 24px;
304 border: 2px outset #c0c0c0 !important;
305 cursor: pointer;
306 position: relative;
307 padding: 0 !important;
308 margin: 0 !important;
309 background-image: none !important;
310 min-width: 24px !important;
311 min-height: 24px !important;
312 max-width: 24px !important;
313 max-height: 24px !important;
314 box-sizing: border-box !important;
315 flex-shrink: 0;
316 transition: all 0.2s ease;
317 }
318
319 .gradient-slot.active {
320 border: 2px inset #c0c0c0 !important;
321 box-shadow: inset 0 0 0 1px #000 !important;
322 }
323
324 .gradient-slot:hover {
325 transform: scale(1.1);
326 }
327
328 .gradient-slot .remove-slot {
329 position: absolute;
330 top: -4px;
331 right: -4px;
332 width: 12px !important;
333 height: 12px !important;
334 min-width: 12px !important;
335 min-height: 12px !important;
336 max-width: 12px !important;
337 max-height: 12px !important;
338 background-color: #808080 !important;
339 color: white !important;
340 border: 1px solid #000 !important;
341 font-size: 9px !important;
342 line-height: 1 !important;
343 cursor: pointer !important;
344 display: inline-flex !important;
345 align-items: center !important;
346 justify-content: center !important;
347 border-radius: 0 !important;
348 padding: 0 !important;
349 margin: 0 !important;
350 font-weight: normal !important;
351 font-family: 'MS Sans Serif', sans-serif !important;
352 z-index: 10;
353 opacity: 0;
354 transition: opacity 0.15s ease;
355 text-align: center !important;
356 box-sizing: border-box !important;
357 user-select: none;
358 }
359
360 .gradient-slot:hover .remove-slot {
361 opacity: 0.8;
362 }
363
364 .gradient-slot .remove-slot:hover {
365 background-color: #000 !important;
366 opacity: 1;
367 }
368
369 .add-slot {
370 width: 24px;
371 height: 24px;
372 border: 2px dashed #666 !important;
373 background: #f0f0f0 !important;
374 cursor: pointer;
375 font-weight: bold;
376 padding: 0 !important;
377 margin: 0 !important;
378 min-width: 24px !important;
379 min-height: 24px !important;
380 max-width: 24px !important;
381 max-height: 24px !important;
382 background-image: none !important;
383 box-sizing: border-box !important;
384 flex-shrink: 0;
385 display: flex !important;
386 align-items: center !important;
387 justify-content: center !important;
388 font-size: 14px !important;
389 color: #666 !important;
390 transition: all 0.2s ease;
391 }
392
393 .add-slot:hover {
394 background: #e0e0e0 !important;
395 border-color: #333 !important;
396 transform: scale(1.05);
397 }
398
399 .gradient-preview {
400 height: 12px;
401 border: 1px inset #dfdfdf;
402 }
403
404 .color-section {
405 margin-bottom: 6px;
406 }
407
408 .section-title {
409 font-size: 10px;
410 font-weight: bold;
411 margin-bottom: 2px;
412 color: #000;
413 }
414
415 .color-row {
416 display: flex;
417 flex-wrap: wrap;
418 gap: 2px;
419 margin-bottom: 4px;
420 padding: 3px;
421 background: #ece9d8;
422 border: 1px inset #dfdfdf;
423 }
424
425 .classic-swatch {
426 width: 18px !important;
427 height: 18px !important;
428 min-width: 18px !important;
429 min-height: 18px !important;
430 }
431
432 .color-grid {
433 display: grid;
434 grid-template-columns: repeat(16, 1fr);
435 gap: 2px;
436 width: 100%;
437 padding: 3px;
438 background: #ece9d8;
439 border: 1px inset #dfdfdf;
440 box-sizing: border-box;
441 }
442
443 .color-swatch {
444 width: 18px !important;
445 height: 18px !important;
446 min-width: 18px !important;
447 min-height: 18px !important;
448 border: 1px solid #808080 !important; /* Override xp.css */
449 cursor: pointer;
450 padding: 0 !important;
451 margin: 0 !important;
452 box-sizing: border-box !important;
453 outline: none !important;
454 /* Remove any xp.css button styling */
455 background-image: none !important;
456 background-color: var(--swatch-color, #000000) !important;
457 font-size: 0 !important; /* Hide any text */
458 line-height: 0 !important;
459 }
460
461 .color-swatch:hover {
462 border: 2px solid #000000 !important;
463 transform: scale(1.1) !important;
464 z-index: 5;
465 position: relative;
466 }
467
468 .color-swatch:active {
469 border: 2px solid #ffffff !important;
470 transform: scale(0.95) !important;
471 }
472
473 .picker-footer {
474 text-align: center;
475 padding-top: 4px;
476 border-top: 1px solid #808080;
477 margin-top: 4px;
478 }
479
480 .color-indicator {
481 border-radius: 1px;
482 }
483</style>