main f6d8405

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>

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