|
7 | 7 | closeColorPicker, |
8 | 8 | getEyedropperActive, |
9 | 9 | setEyedropperActive, |
| 10 | + getColorPickerModel, |
| 11 | + setColorPickerModel, |
| 12 | + COLOR_MODELS, |
| 13 | + type ColorModel, |
10 | 14 | } from '$lib/stores/ui.svelte'; |
11 | 15 | import { |
12 | 16 | getPalette, |
|
40 | 44 | pushRecentColor, |
41 | 45 | } from '$lib/stores/recentColors.svelte'; |
42 | 46 | import ShadeGrid from './ShadeGrid.svelte'; |
| 47 | + import ChannelSlider from './ChannelSlider.svelte'; |
| 48 | + import LockIcon from '$lib/components/shared/LockIcon.svelte'; |
43 | 49 |
|
44 | 50 | const ROLE_LABELS: Record<string, string> = { |
45 | 51 | background: 'Background', |
|
186 | 192 |
|
187 | 193 | const HARMONY: {label: string; delta: number; title: string}[] = [ |
188 | 194 | {label: 'An−', delta: -30, title: 'Analogous −30°'}, |
189 | | - {label: 'Tri', delta: 120, title: 'Triad +120°'}, |
| 195 | + {label: 'Tri+', delta: 120, title: 'Triad +120°'}, |
190 | 196 | {label: 'Comp', delta: 180, title: 'Complement +180°'}, |
191 | | - {label: 'Tri', delta: 240, title: 'Triad +240°'}, |
| 197 | + {label: 'Tri−', delta: 240, title: 'Triad +240° (−120°)'}, |
192 | 198 | {label: 'An+', delta: 30, title: 'Analogous +30°'}, |
193 | 199 | ]; |
194 | 200 |
|
| 201 | + const MODEL_LABELS: Record<ColorModel, string> = { |
| 202 | + rgb: 'RGB', |
| 203 | + hsl: 'HSL', |
| 204 | + oklch: 'OKLCH', |
| 205 | + }; |
| 206 | + let activeModel = $derived(getColorPickerModel()); |
| 207 | +
|
195 | 208 | let harmonyColors = $derived( |
196 | 209 | HARMONY.map(h => ({ |
197 | 210 | ...h, |
|
402 | 415 | <div class="space-y-4 p-4"> |
403 | 416 | <div class="flex gap-3"> |
404 | 417 | <label |
405 | | - class="border-border relative h-20 w-20 shrink-0 cursor-pointer overflow-hidden border" |
| 418 | + class="border-border relative h-20 w-20 shrink-0 overflow-hidden border {locked |
| 419 | + ? 'cursor-not-allowed' |
| 420 | + : 'cursor-pointer'}" |
406 | 421 | > |
407 | 422 | <div |
408 | 423 | class="absolute inset-0" |
409 | 424 | style:background-color={currentColor} |
410 | 425 | ></div> |
411 | | - {#if !locked} |
| 426 | + {#if locked} |
| 427 | + <div |
| 428 | + class="absolute inset-0 flex items-center justify-center bg-black/45 text-white" |
| 429 | + > |
| 430 | + <LockIcon locked={true} size="h-6 w-6" /> |
| 431 | + </div> |
| 432 | + {:else} |
412 | 433 | <input |
413 | 434 | type="color" |
414 | 435 | value={currentColor} |
|
426 | 447 | > |
427 | 448 | <input |
428 | 449 | type="text" |
429 | | - class="text-fg-primary bg-bg-secondary w-full border px-2.5 py-1.5 font-mono text-[13px] outline-none |
| 450 | + class="text-fg-primary bg-bg-secondary w-full border px-2.5 py-1.5 font-mono text-[13px] outline-none disabled:cursor-not-allowed disabled:opacity-50 |
430 | 451 | {isValid |
431 | 452 | ? 'focus:border-accent border-border' |
432 | 453 | : 'border-destructive'}" |
|
440 | 461 | </div> |
441 | 462 | {#if !isExtended && !isOverride} |
442 | 463 | <div class="mt-2 flex items-center justify-between"> |
443 | | - <span class="text-fg-dimmed text-[10px]">Locked</span> |
| 464 | + <span |
| 465 | + class="text-fg-dimmed text-[9px] uppercase tracking-wider" |
| 466 | + >Lock</span |
| 467 | + > |
444 | 468 | <button |
445 | 469 | class="relative h-5 w-9 transition-colors duration-150 |
446 | 470 | {locked |
|
517 | 541 | </div> |
518 | 542 | </div> |
519 | 543 |
|
520 | | - <div class="space-y-2"> |
521 | | - <span class="text-fg-dimmed text-[9px] uppercase tracking-wider" |
522 | | - >RGB Channels</span |
523 | | - > |
524 | | - {#each RGB_CHANNELS as channel} |
525 | | - <div class="flex items-center gap-2"> |
526 | | - <span class="text-fg-dimmed w-3 font-mono text-[10px]" |
527 | | - >{RGB_LABELS[channel]}</span |
528 | | - > |
529 | | - <div |
530 | | - class="relative h-3 flex-1" |
531 | | - style="background: {channelGradient( |
532 | | - channel |
533 | | - )}; border: 1px solid var(--color-border);" |
534 | | - > |
535 | | - <input |
536 | | - type="range" |
537 | | - class="absolute inset-0 h-full w-full cursor-pointer opacity-0" |
538 | | - min="0" |
539 | | - max="255" |
540 | | - step="1" |
541 | | - value={rgb[channel]} |
542 | | - oninput={e => |
543 | | - handleRgbChange( |
544 | | - channel, |
545 | | - parseInt(e.currentTarget.value) |
546 | | - )} |
547 | | - disabled={locked} |
548 | | - /> |
549 | | - <div |
550 | | - class="pointer-events-none absolute bottom-0 top-0 w-0.5 bg-white shadow-sm" |
551 | | - style:left="{(rgb[channel] / 255) * 100}%" |
552 | | - ></div> |
553 | | - </div> |
554 | | - <span |
555 | | - class="text-fg-dimmed w-7 text-right font-mono text-[10px]" |
556 | | - >{rgb[channel]}</span |
557 | | - > |
558 | | - </div> |
559 | | - {/each} |
560 | | - </div> |
561 | | - |
562 | | - <div class="space-y-2"> |
563 | | - <span class="text-fg-dimmed text-[9px] uppercase tracking-wider" |
564 | | - >HSL Channels</span |
565 | | - > |
566 | | - {#each HSL_CHANNELS as channel} |
567 | | - <div class="flex items-center gap-2"> |
568 | | - <span class="text-fg-dimmed w-3 font-mono text-[10px]" |
569 | | - >{HSL_LABELS[channel]}</span |
570 | | - > |
571 | | - <div |
572 | | - class="relative h-3 flex-1" |
573 | | - style="background: {hslChannelGradient( |
574 | | - channel |
575 | | - )}; border: 1px solid var(--color-border);" |
576 | | - > |
577 | | - <input |
578 | | - type="range" |
579 | | - class="absolute inset-0 h-full w-full cursor-pointer opacity-0" |
580 | | - min="0" |
581 | | - max={HSL_MAX[channel]} |
582 | | - step="1" |
583 | | - value={hsl[channel]} |
584 | | - oninput={e => |
585 | | - handleHslChange( |
586 | | - channel, |
587 | | - parseFloat(e.currentTarget.value) |
588 | | - )} |
589 | | - disabled={locked} |
590 | | - /> |
591 | | - <div |
592 | | - class="pointer-events-none absolute bottom-0 top-0 w-0.5 bg-white shadow-sm" |
593 | | - style:left="{(hsl[channel] / HSL_MAX[channel]) * |
594 | | - 100}%" |
595 | | - ></div> |
596 | | - </div> |
597 | | - <span |
598 | | - class="text-fg-dimmed w-7 text-right font-mono text-[10px] tabular-nums" |
599 | | - >{Math.round(hsl[channel])}{HSL_SUFFIX[channel]}</span |
| 544 | + <div class="space-y-2.5"> |
| 545 | + <div class="flex items-center gap-1"> |
| 546 | + {#each COLOR_MODELS as id} |
| 547 | + <button |
| 548 | + type="button" |
| 549 | + class="border px-2 py-0.5 text-[9px] uppercase tracking-wider transition-colors |
| 550 | + {activeModel === id |
| 551 | + ? 'text-accent border-accent bg-accent-muted' |
| 552 | + : 'text-fg-dimmed border-border hover:text-fg-secondary'}" |
| 553 | + onclick={() => setColorPickerModel(id)} |
| 554 | + aria-pressed={activeModel === id} |
| 555 | + >{MODEL_LABELS[id]}</button |
600 | 556 | > |
601 | | - </div> |
602 | | - {/each} |
603 | | - </div> |
| 557 | + {/each} |
| 558 | + </div> |
604 | 559 |
|
605 | | - <div class="space-y-2"> |
606 | | - <span class="text-fg-dimmed text-[9px] uppercase tracking-wider" |
607 | | - >OKLCH Channels</span |
608 | | - > |
609 | | - {#each OKLCH_CHANNELS as channel} |
610 | | - <div class="flex items-center gap-2"> |
611 | | - <span class="text-fg-dimmed w-3 font-mono text-[10px]" |
612 | | - >{OKLCH_LABELS[channel]}</span |
613 | | - > |
614 | | - <div |
615 | | - class="relative h-3 flex-1" |
616 | | - style="background: {oklchChannelGradient( |
617 | | - channel |
618 | | - )}; border: 1px solid var(--color-border);" |
619 | | - > |
620 | | - <input |
621 | | - type="range" |
622 | | - class="absolute inset-0 h-full w-full cursor-pointer opacity-0" |
623 | | - min="0" |
624 | | - max={OKLCH_MAX[channel]} |
625 | | - step="1" |
626 | | - value={oklchSlider[channel]} |
627 | | - oninput={e => |
628 | | - handleOklchChange( |
629 | | - channel, |
630 | | - parseFloat(e.currentTarget.value) |
631 | | - )} |
632 | | - disabled={locked} |
633 | | - /> |
634 | | - <div |
635 | | - class="pointer-events-none absolute bottom-0 top-0 w-0.5 bg-white shadow-sm" |
636 | | - style:left="{(oklchSlider[channel] / |
637 | | - OKLCH_MAX[channel]) * |
638 | | - 100}%" |
639 | | - ></div> |
640 | | - </div> |
641 | | - <span |
642 | | - class="text-fg-dimmed w-10 text-right font-mono text-[10px] tabular-nums" |
643 | | - >{formatOklch(channel)}</span |
644 | | - > |
645 | | - </div> |
646 | | - {/each} |
| 560 | + {#if activeModel === 'rgb'} |
| 561 | + {#each RGB_CHANNELS as channel} |
| 562 | + <ChannelSlider |
| 563 | + label={RGB_LABELS[channel]} |
| 564 | + value={rgb[channel]} |
| 565 | + max={255} |
| 566 | + display={String(rgb[channel])} |
| 567 | + gradient={channelGradient(channel)} |
| 568 | + disabled={locked} |
| 569 | + onchange={v => handleRgbChange(channel, Math.round(v))} |
| 570 | + /> |
| 571 | + {/each} |
| 572 | + {:else if activeModel === 'hsl'} |
| 573 | + {#each HSL_CHANNELS as channel} |
| 574 | + <ChannelSlider |
| 575 | + label={HSL_LABELS[channel]} |
| 576 | + value={hsl[channel]} |
| 577 | + max={HSL_MAX[channel]} |
| 578 | + display={`${Math.round(hsl[channel])}${HSL_SUFFIX[channel]}`} |
| 579 | + gradient={hslChannelGradient(channel)} |
| 580 | + disabled={locked} |
| 581 | + onchange={v => handleHslChange(channel, v)} |
| 582 | + /> |
| 583 | + {/each} |
| 584 | + {:else} |
| 585 | + {#each OKLCH_CHANNELS as channel} |
| 586 | + <ChannelSlider |
| 587 | + label={OKLCH_LABELS[channel]} |
| 588 | + value={oklchSlider[channel]} |
| 589 | + max={OKLCH_MAX[channel]} |
| 590 | + display={formatOklch(channel)} |
| 591 | + gradient={oklchChannelGradient(channel)} |
| 592 | + disabled={locked} |
| 593 | + onchange={v => handleOklchChange(channel, v)} |
| 594 | + /> |
| 595 | + {/each} |
| 596 | + {/if} |
647 | 597 | </div> |
648 | 598 |
|
649 | 599 | {#if getRecentColors().length > 0} |
|
0 commit comments