Skip to content

Commit c231289

Browse files
committed
Allow editing RGB/HSL/OKLCH values by clicking them
1 parent fc5efb5 commit c231289

3 files changed

Lines changed: 99 additions & 7 deletions

File tree

frontend/src/lib/components/color-picker/ChannelSlider.svelte

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
gradient,
99
disabled = false,
1010
onchange,
11+
oncommit,
1112
}: {
1213
label: string;
1314
value: number;
@@ -17,9 +18,45 @@
1718
gradient: string;
1819
disabled?: boolean;
1920
onchange: (value: number) => void;
21+
oncommit: (raw: string) => void;
2022
} = $props();
2123
2224
let percent = $derived((value / max) * 100);
25+
26+
let editing = $state(false);
27+
let editValue = $state('');
28+
let inputEl = $state<HTMLInputElement | null>(null);
29+
30+
$effect(() => {
31+
if (editing && inputEl) {
32+
inputEl.focus();
33+
inputEl.select();
34+
}
35+
});
36+
37+
function startEdit() {
38+
if (disabled) return;
39+
editValue = display;
40+
editing = true;
41+
}
42+
43+
// Guarded so Enter's commit() doesn't double-fire when the subsequent
44+
// onblur (from the unmounting input) would otherwise run commit again.
45+
function commit() {
46+
if (!editing) return;
47+
oncommit(editValue);
48+
editing = false;
49+
}
50+
51+
function handleKey(e: KeyboardEvent) {
52+
if (e.key === 'Enter') {
53+
e.preventDefault();
54+
commit();
55+
} else if (e.key === 'Escape') {
56+
e.preventDefault();
57+
editing = false;
58+
}
59+
}
2360
</script>
2461

2562
<div class="flex items-center gap-2">
@@ -41,17 +78,30 @@
4178
aria-label={label}
4279
aria-valuetext={display}
4380
/>
44-
<!-- Thumb: white bar with subtle dark ring + drop shadow so it's legible
45-
on any gradient. Offset by half its width so the value position is
46-
dead-center of the bar, not at its left edge. -->
4781
<div
4882
class="pointer-events-none absolute inset-y-[-1px] w-[3px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.4),0_1px_3px_rgba(0,0,0,0.35)] transition-[width] group-hover:w-1"
4983
style:left="calc({percent}% - 1.5px)"
5084
></div>
5185
</div>
5286

53-
<span
54-
class="text-fg-dimmed w-12 text-right font-mono text-[10px] tabular-nums"
55-
>{display}</span
56-
>
87+
{#if editing}
88+
<input
89+
type="text"
90+
bind:this={inputEl}
91+
bind:value={editValue}
92+
onblur={commit}
93+
onkeydown={handleKey}
94+
spellcheck={false}
95+
class="text-fg-primary bg-bg-secondary border-accent w-12 border px-1 text-right font-mono text-[10px] tabular-nums outline-none"
96+
/>
97+
{:else}
98+
<button
99+
type="button"
100+
class="text-fg-dimmed w-12 text-right font-mono text-[10px] tabular-nums transition-colors
101+
{disabled ? 'cursor-default' : 'hover:text-fg-primary'}"
102+
onclick={startEdit}
103+
{disabled}
104+
aria-label="Edit {label} value">{display}</button
105+
>
106+
{/if}
57107
</div>

frontend/src/lib/components/color-picker/ColorPickerDialog.svelte

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import ShadeGrid from './ShadeGrid.svelte';
4747
import ChannelSlider from './ChannelSlider.svelte';
4848
import LockIcon from '$lib/components/shared/LockIcon.svelte';
49+
import {clamp} from '$lib/utils/math';
4950
5051
const ROLE_LABELS: Record<string, string> = {
5152
background: 'Background',
@@ -205,6 +206,26 @@
205206
};
206207
let activeModel = $derived(getColorPickerModel());
207208
209+
function parseEditedNumber(raw: string): number | null {
210+
const n = parseFloat(raw.replace(/[°%]/g, '').trim());
211+
return Number.isFinite(n) ? n : null;
212+
}
213+
214+
// Builds an `oncommit` handler for a ChannelSlider: parse → clamp → dispatch.
215+
// `transform` lets OKLCH C map its 0..0.4 display value onto the 0..40 slider.
216+
function makeCommitHandler<C extends string>(
217+
channel: C,
218+
handle: (c: C, value: number) => void,
219+
max: number,
220+
transform: (n: number) => number = n => n
221+
): (raw: string) => void {
222+
return raw => {
223+
const n = parseEditedNumber(raw);
224+
if (n === null) return;
225+
handle(channel, clamp(transform(n), 0, max));
226+
};
227+
}
228+
208229
let harmonyColors = $derived(
209230
HARMONY.map(h => ({
210231
...h,
@@ -567,6 +588,12 @@
567588
gradient={channelGradient(channel)}
568589
disabled={locked}
569590
onchange={v => handleRgbChange(channel, Math.round(v))}
591+
oncommit={makeCommitHandler(
592+
channel,
593+
handleRgbChange,
594+
255,
595+
Math.round
596+
)}
570597
/>
571598
{/each}
572599
{:else if activeModel === 'hsl'}
@@ -579,6 +606,11 @@
579606
gradient={hslChannelGradient(channel)}
580607
disabled={locked}
581608
onchange={v => handleHslChange(channel, v)}
609+
oncommit={makeCommitHandler(
610+
channel,
611+
handleHslChange,
612+
HSL_MAX[channel]
613+
)}
582614
/>
583615
{/each}
584616
{:else}
@@ -591,6 +623,13 @@
591623
gradient={oklchChannelGradient(channel)}
592624
disabled={locked}
593625
onchange={v => handleOklchChange(channel, v)}
626+
oncommit={makeCommitHandler(
627+
channel,
628+
handleOklchChange,
629+
OKLCH_MAX[channel],
630+
// C displays 0..0.4 but slider is 0..40.
631+
channel === 'c' ? n => n * 100 : undefined
632+
)}
594633
/>
595634
{/each}
596635
{/if}

frontend/src/lib/utils/math.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function clamp(n: number, lo: number, hi: number): number {
2+
return Math.max(lo, Math.min(hi, n));
3+
}

0 commit comments

Comments
 (0)