vzcode 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-BK1qsxog.js → index-858eZSOF.js} +98 -98
- package/dist/assets/{index-C5d5win4.css → index-DLm5FQ0E.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/client/VZSidebar/VisualEditor/CheckboxWidget.tsx +46 -0
- package/src/client/VZSidebar/VisualEditor/ColorWidget.tsx +173 -0
- package/src/client/VZSidebar/VisualEditor/DropdownWidget.tsx +84 -0
- package/src/client/VZSidebar/VisualEditor/SliderWidget.tsx +63 -0
- package/src/client/VZSidebar/VisualEditor/TextInputWidget.tsx +36 -0
- package/src/client/VZSidebar/{VisualEditor.tsx → VisualEditor/VisualEditor.tsx} +61 -334
- package/src/client/VZSidebar/VisualEditor/constants.ts +1 -0
- package/src/client/VZSidebar/VisualEditor/index.tsx +1 -0
- package/src/client/VZSidebar/VisualEditor/utils.ts +102 -0
- package/src/client/VZSidebar/styles.scss +32 -21
|
@@ -5,35 +5,17 @@ import {
|
|
|
5
5
|
useEffect,
|
|
6
6
|
useMemo,
|
|
7
7
|
} from 'react';
|
|
8
|
-
import { VZCodeContext } from '
|
|
8
|
+
import { VZCodeContext } from '../../VZCodeContext';
|
|
9
9
|
import { VizContent, VizFileId } from '@vizhub/viz-types';
|
|
10
|
-
import { VisualEditorConfigEntry } from '
|
|
11
|
-
import { EmptyState } from '
|
|
10
|
+
import { VisualEditorConfigEntry } from '../../../types';
|
|
11
|
+
import { EmptyState } from '../EmptyState';
|
|
12
12
|
import { color, hcl, HCLColor, rgb } from 'd3-color';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
min: number,
|
|
20
|
-
max: number,
|
|
21
|
-
) => {
|
|
22
|
-
return ((value - min) / (max - min)) * 100;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Helper function to format values nicely
|
|
26
|
-
const formatValue = (
|
|
27
|
-
value: number,
|
|
28
|
-
min: number,
|
|
29
|
-
max: number,
|
|
30
|
-
) => {
|
|
31
|
-
// Determine decimal places based on the range
|
|
32
|
-
const range = max - min;
|
|
33
|
-
const decimalPlaces =
|
|
34
|
-
range < 10 ? 2 : range < 100 ? 1 : 0;
|
|
35
|
-
return Number(value).toFixed(decimalPlaces);
|
|
36
|
-
};
|
|
13
|
+
import { CONFIG_FILE_NAME } from './constants';
|
|
14
|
+
import { SliderWidget } from './SliderWidget';
|
|
15
|
+
import { CheckboxWidget } from './CheckboxWidget';
|
|
16
|
+
import { TextInputWidget } from './TextInputWidget';
|
|
17
|
+
import { DropdownWidget } from './DropdownWidget';
|
|
18
|
+
import { ColorWidget } from './ColorWidget';
|
|
37
19
|
|
|
38
20
|
export const VisualEditor = () => {
|
|
39
21
|
const { files, submitOperation } =
|
|
@@ -418,59 +400,20 @@ export const VisualEditor = () => {
|
|
|
418
400
|
const currentValue =
|
|
419
401
|
localValues[widgetConfig.property] ??
|
|
420
402
|
configData[widgetConfig.property];
|
|
421
|
-
const percentage = calculatePercentage(
|
|
422
|
-
currentValue,
|
|
423
|
-
widgetConfig.min,
|
|
424
|
-
widgetConfig.max,
|
|
425
|
-
);
|
|
426
403
|
|
|
427
404
|
return (
|
|
428
|
-
<
|
|
405
|
+
<SliderWidget
|
|
429
406
|
key={widgetConfig.property}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
{formatValue(
|
|
441
|
-
currentValue,
|
|
442
|
-
widgetConfig.min,
|
|
443
|
-
widgetConfig.max,
|
|
444
|
-
)}
|
|
445
|
-
</span>
|
|
446
|
-
</div>
|
|
447
|
-
<div className="slider-container">
|
|
448
|
-
<input
|
|
449
|
-
type="range"
|
|
450
|
-
id={widgetConfig.property}
|
|
451
|
-
className="slider-input"
|
|
452
|
-
min={widgetConfig.min}
|
|
453
|
-
max={widgetConfig.max}
|
|
454
|
-
step={widgetConfig.step}
|
|
455
|
-
value={currentValue}
|
|
456
|
-
onChange={onSliderChange(
|
|
457
|
-
widgetConfig.property,
|
|
458
|
-
)}
|
|
459
|
-
/>
|
|
460
|
-
<div
|
|
461
|
-
className="slider-track-fill"
|
|
462
|
-
style={{ width: `${percentage}%` }}
|
|
463
|
-
/>
|
|
464
|
-
</div>
|
|
465
|
-
<div className="slider-bounds">
|
|
466
|
-
<span className="min-value">
|
|
467
|
-
{widgetConfig.min}
|
|
468
|
-
</span>
|
|
469
|
-
<span className="max-value">
|
|
470
|
-
{widgetConfig.max}
|
|
471
|
-
</span>
|
|
472
|
-
</div>
|
|
473
|
-
</div>
|
|
407
|
+
property={widgetConfig.property}
|
|
408
|
+
label={widgetConfig.label}
|
|
409
|
+
min={widgetConfig.min}
|
|
410
|
+
max={widgetConfig.max}
|
|
411
|
+
step={widgetConfig.step}
|
|
412
|
+
currentValue={currentValue}
|
|
413
|
+
onChange={onSliderChange(
|
|
414
|
+
widgetConfig.property,
|
|
415
|
+
)}
|
|
416
|
+
/>
|
|
474
417
|
);
|
|
475
418
|
} else if (widgetConfig.type === 'checkbox') {
|
|
476
419
|
// Use local value if available, otherwise fall back to config value
|
|
@@ -479,40 +422,15 @@ export const VisualEditor = () => {
|
|
|
479
422
|
configData[widgetConfig.property];
|
|
480
423
|
|
|
481
424
|
return (
|
|
482
|
-
<
|
|
425
|
+
<CheckboxWidget
|
|
483
426
|
key={widgetConfig.property}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
{widgetConfig.label}
|
|
492
|
-
</label>
|
|
493
|
-
<span className="checkbox-value">
|
|
494
|
-
{currentValue ? 'On' : 'Off'}
|
|
495
|
-
</span>
|
|
496
|
-
</div>
|
|
497
|
-
<div className="checkbox-container">
|
|
498
|
-
<input
|
|
499
|
-
type="checkbox"
|
|
500
|
-
id={widgetConfig.property}
|
|
501
|
-
className="checkbox-input"
|
|
502
|
-
checked={currentValue}
|
|
503
|
-
onChange={onCheckboxChange(
|
|
504
|
-
widgetConfig.property,
|
|
505
|
-
)}
|
|
506
|
-
/>
|
|
507
|
-
<div className="checkbox-visual">
|
|
508
|
-
<div
|
|
509
|
-
className={`checkbox-indicator ${
|
|
510
|
-
currentValue ? 'checked' : ''
|
|
511
|
-
}`}
|
|
512
|
-
/>
|
|
513
|
-
</div>
|
|
514
|
-
</div>
|
|
515
|
-
</div>
|
|
427
|
+
property={widgetConfig.property}
|
|
428
|
+
label={widgetConfig.label}
|
|
429
|
+
currentValue={currentValue}
|
|
430
|
+
onChange={onCheckboxChange(
|
|
431
|
+
widgetConfig.property,
|
|
432
|
+
)}
|
|
433
|
+
/>
|
|
516
434
|
);
|
|
517
435
|
} else if (widgetConfig.type === 'textInput') {
|
|
518
436
|
// Use local value if available, otherwise fall back to config value
|
|
@@ -521,30 +439,15 @@ export const VisualEditor = () => {
|
|
|
521
439
|
configData[widgetConfig.property];
|
|
522
440
|
|
|
523
441
|
return (
|
|
524
|
-
<
|
|
442
|
+
<TextInputWidget
|
|
525
443
|
key={widgetConfig.property}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
{widgetConfig.label}
|
|
534
|
-
</label>
|
|
535
|
-
</div>
|
|
536
|
-
<div className="text-input-container">
|
|
537
|
-
<input
|
|
538
|
-
type="text"
|
|
539
|
-
id={widgetConfig.property}
|
|
540
|
-
className="text-input-field"
|
|
541
|
-
value={currentValue || ''}
|
|
542
|
-
onChange={onTextInputChange(
|
|
543
|
-
widgetConfig.property,
|
|
544
|
-
)}
|
|
545
|
-
/>
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
444
|
+
property={widgetConfig.property}
|
|
445
|
+
label={widgetConfig.label}
|
|
446
|
+
currentValue={currentValue}
|
|
447
|
+
onChange={onTextInputChange(
|
|
448
|
+
widgetConfig.property,
|
|
449
|
+
)}
|
|
450
|
+
/>
|
|
548
451
|
);
|
|
549
452
|
} else if (widgetConfig.type === 'dropdown') {
|
|
550
453
|
// Use local value if available, otherwise fall back to config value
|
|
@@ -555,85 +458,23 @@ export const VisualEditor = () => {
|
|
|
555
458
|
openDropdown === widgetConfig.property;
|
|
556
459
|
|
|
557
460
|
return (
|
|
558
|
-
<
|
|
461
|
+
<DropdownWidget
|
|
559
462
|
key={widgetConfig.property}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
type="button"
|
|
576
|
-
className={`dropdown-button ${
|
|
577
|
-
isOpen ? 'open' : ''
|
|
578
|
-
}`}
|
|
579
|
-
onClick={() =>
|
|
580
|
-
handleDropdownToggle(
|
|
581
|
-
widgetConfig.property,
|
|
582
|
-
)
|
|
583
|
-
}
|
|
584
|
-
aria-expanded={isOpen}
|
|
585
|
-
aria-haspopup="listbox"
|
|
586
|
-
>
|
|
587
|
-
<span className="dropdown-button-text">
|
|
588
|
-
{currentValue}
|
|
589
|
-
</span>
|
|
590
|
-
<div
|
|
591
|
-
className={`dropdown-arrow ${
|
|
592
|
-
isOpen ? 'open' : ''
|
|
593
|
-
}`}
|
|
594
|
-
>
|
|
595
|
-
<svg
|
|
596
|
-
width="12"
|
|
597
|
-
height="8"
|
|
598
|
-
viewBox="0 0 12 8"
|
|
599
|
-
fill="none"
|
|
600
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
601
|
-
>
|
|
602
|
-
<path
|
|
603
|
-
d="M1 1.5L6 6.5L11 1.5"
|
|
604
|
-
stroke="currentColor"
|
|
605
|
-
strokeWidth="2"
|
|
606
|
-
strokeLinecap="round"
|
|
607
|
-
strokeLinejoin="round"
|
|
608
|
-
/>
|
|
609
|
-
</svg>
|
|
610
|
-
</div>
|
|
611
|
-
</button>
|
|
612
|
-
{isOpen && (
|
|
613
|
-
<div className="dropdown-options">
|
|
614
|
-
{widgetConfig.options.map((option) => (
|
|
615
|
-
<button
|
|
616
|
-
key={option}
|
|
617
|
-
type="button"
|
|
618
|
-
className={`dropdown-option ${
|
|
619
|
-
option === currentValue
|
|
620
|
-
? 'selected'
|
|
621
|
-
: ''
|
|
622
|
-
}`}
|
|
623
|
-
onClick={() =>
|
|
624
|
-
handleDropdownOptionClick(
|
|
625
|
-
widgetConfig.property,
|
|
626
|
-
option,
|
|
627
|
-
)
|
|
628
|
-
}
|
|
629
|
-
>
|
|
630
|
-
{option}
|
|
631
|
-
</button>
|
|
632
|
-
))}
|
|
633
|
-
</div>
|
|
634
|
-
)}
|
|
635
|
-
</div>
|
|
636
|
-
</div>
|
|
463
|
+
property={widgetConfig.property}
|
|
464
|
+
label={widgetConfig.label}
|
|
465
|
+
options={widgetConfig.options}
|
|
466
|
+
currentValue={currentValue}
|
|
467
|
+
isOpen={isOpen}
|
|
468
|
+
onToggle={() =>
|
|
469
|
+
handleDropdownToggle(widgetConfig.property)
|
|
470
|
+
}
|
|
471
|
+
onOptionClick={(value) =>
|
|
472
|
+
handleDropdownOptionClick(
|
|
473
|
+
widgetConfig.property,
|
|
474
|
+
value,
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
/>
|
|
637
478
|
);
|
|
638
479
|
} else if (widgetConfig.type === 'color') {
|
|
639
480
|
// Use local value if available, otherwise fall back to config value
|
|
@@ -663,130 +504,16 @@ export const VisualEditor = () => {
|
|
|
663
504
|
hclColor.l;
|
|
664
505
|
|
|
665
506
|
return (
|
|
666
|
-
<
|
|
507
|
+
<ColorWidget
|
|
667
508
|
key={widgetConfig.property}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
</span>
|
|
677
|
-
</div>
|
|
678
|
-
|
|
679
|
-
<div
|
|
680
|
-
className="color-preview"
|
|
681
|
-
style={{ backgroundColor: currentHex }}
|
|
682
|
-
/>
|
|
683
|
-
|
|
684
|
-
<div className="lch-sliders">
|
|
685
|
-
{/* Lightness Slider */}
|
|
686
|
-
<div className="lch-slider">
|
|
687
|
-
<div className="slider-header">
|
|
688
|
-
<label className="slider-label">
|
|
689
|
-
Lightness
|
|
690
|
-
</label>
|
|
691
|
-
<span className="slider-value">
|
|
692
|
-
{Math.round(currentL)}
|
|
693
|
-
</span>
|
|
694
|
-
</div>
|
|
695
|
-
<div className="slider-container">
|
|
696
|
-
<input
|
|
697
|
-
type="range"
|
|
698
|
-
className="slider-input"
|
|
699
|
-
min={0}
|
|
700
|
-
max={100}
|
|
701
|
-
step={1}
|
|
702
|
-
value={currentL}
|
|
703
|
-
onChange={onColorChange(
|
|
704
|
-
widgetConfig.property,
|
|
705
|
-
'l',
|
|
706
|
-
)}
|
|
707
|
-
/>
|
|
708
|
-
<div
|
|
709
|
-
className="slider-track-fill"
|
|
710
|
-
style={{ width: `${currentL}%` }}
|
|
711
|
-
/>
|
|
712
|
-
</div>
|
|
713
|
-
<div className="slider-bounds">
|
|
714
|
-
<span className="min-value">0</span>
|
|
715
|
-
<span className="max-value">100</span>
|
|
716
|
-
</div>
|
|
717
|
-
</div>
|
|
718
|
-
|
|
719
|
-
{/* Chroma Slider */}
|
|
720
|
-
<div className="lch-slider">
|
|
721
|
-
<div className="slider-header">
|
|
722
|
-
<label className="slider-label">
|
|
723
|
-
Chroma
|
|
724
|
-
</label>
|
|
725
|
-
<span className="slider-value">
|
|
726
|
-
{Math.round(currentC)}
|
|
727
|
-
</span>
|
|
728
|
-
</div>
|
|
729
|
-
<div className="slider-container">
|
|
730
|
-
<input
|
|
731
|
-
type="range"
|
|
732
|
-
className="slider-input"
|
|
733
|
-
min={0}
|
|
734
|
-
max={100}
|
|
735
|
-
step={1}
|
|
736
|
-
value={currentC}
|
|
737
|
-
onChange={onColorChange(
|
|
738
|
-
widgetConfig.property,
|
|
739
|
-
'c',
|
|
740
|
-
)}
|
|
741
|
-
/>
|
|
742
|
-
<div
|
|
743
|
-
className="slider-track-fill"
|
|
744
|
-
style={{ width: `${currentC}%` }}
|
|
745
|
-
/>
|
|
746
|
-
</div>
|
|
747
|
-
<div className="slider-bounds">
|
|
748
|
-
<span className="min-value">0</span>
|
|
749
|
-
<span className="max-value">100</span>
|
|
750
|
-
</div>
|
|
751
|
-
</div>
|
|
752
|
-
|
|
753
|
-
{/* Hue Slider */}
|
|
754
|
-
<div className="lch-slider">
|
|
755
|
-
<div className="slider-header">
|
|
756
|
-
<label className="slider-label">
|
|
757
|
-
Hue
|
|
758
|
-
</label>
|
|
759
|
-
<span className="slider-value">
|
|
760
|
-
{Math.round(currentH)}°
|
|
761
|
-
</span>
|
|
762
|
-
</div>
|
|
763
|
-
<div className="slider-container">
|
|
764
|
-
<input
|
|
765
|
-
type="range"
|
|
766
|
-
className="slider-input"
|
|
767
|
-
min={0}
|
|
768
|
-
max={360}
|
|
769
|
-
step={1}
|
|
770
|
-
value={currentH}
|
|
771
|
-
onChange={onColorChange(
|
|
772
|
-
widgetConfig.property,
|
|
773
|
-
'h',
|
|
774
|
-
)}
|
|
775
|
-
/>
|
|
776
|
-
<div
|
|
777
|
-
className="slider-track-fill"
|
|
778
|
-
style={{
|
|
779
|
-
width: `${(currentH / 360) * 100}%`,
|
|
780
|
-
}}
|
|
781
|
-
/>
|
|
782
|
-
</div>
|
|
783
|
-
<div className="slider-bounds">
|
|
784
|
-
<span className="min-value">0°</span>
|
|
785
|
-
<span className="max-value">360°</span>
|
|
786
|
-
</div>
|
|
787
|
-
</div>
|
|
788
|
-
</div>
|
|
789
|
-
</div>
|
|
509
|
+
property={widgetConfig.property}
|
|
510
|
+
label={widgetConfig.label}
|
|
511
|
+
currentHex={currentHex}
|
|
512
|
+
currentH={currentH}
|
|
513
|
+
currentC={currentC}
|
|
514
|
+
currentL={currentL}
|
|
515
|
+
onColorChange={onColorChange}
|
|
516
|
+
/>
|
|
790
517
|
);
|
|
791
518
|
}
|
|
792
519
|
})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CONFIG_FILE_NAME = 'config.json';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VisualEditor } from './VisualEditor';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { hcl } from 'd3-color';
|
|
2
|
+
|
|
3
|
+
// Helper function to calculate percentage for progress fill
|
|
4
|
+
export const calculatePercentage = (
|
|
5
|
+
value: number,
|
|
6
|
+
min: number,
|
|
7
|
+
max: number,
|
|
8
|
+
) => {
|
|
9
|
+
return ((value - min) / (max - min)) * 100;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Helper function to format values nicely
|
|
13
|
+
export const formatValue = (
|
|
14
|
+
value: number,
|
|
15
|
+
min: number,
|
|
16
|
+
max: number,
|
|
17
|
+
) => {
|
|
18
|
+
// Determine decimal places based on the range
|
|
19
|
+
const range = max - min;
|
|
20
|
+
const decimalPlaces =
|
|
21
|
+
range < 10 ? 2 : range < 100 ? 1 : 0;
|
|
22
|
+
return Number(value).toFixed(decimalPlaces);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Helper function to check if HCL color is displayable
|
|
26
|
+
export const hclOk = (
|
|
27
|
+
h: number,
|
|
28
|
+
c: number,
|
|
29
|
+
l: number,
|
|
30
|
+
): boolean => {
|
|
31
|
+
try {
|
|
32
|
+
const color = hcl(h, c, l);
|
|
33
|
+
return color.displayable();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Helper function to render slider background gradients
|
|
40
|
+
export const renderSliderBackground = (
|
|
41
|
+
canvas: HTMLCanvasElement | null,
|
|
42
|
+
channel: 'h' | 'c' | 'l',
|
|
43
|
+
values: { h: number; c: number; l: number },
|
|
44
|
+
): void => {
|
|
45
|
+
if (!canvas) return;
|
|
46
|
+
|
|
47
|
+
const width = canvas.width;
|
|
48
|
+
const height = canvas.height;
|
|
49
|
+
const ctx = canvas.getContext('2d');
|
|
50
|
+
if (!ctx) return;
|
|
51
|
+
|
|
52
|
+
const imageData = ctx.createImageData(width, height);
|
|
53
|
+
|
|
54
|
+
for (let x = 0; x < width; x++) {
|
|
55
|
+
let h, c, l;
|
|
56
|
+
|
|
57
|
+
// Map x pixel to value in slider domain
|
|
58
|
+
if (channel === 'h') {
|
|
59
|
+
const val = (x * 360) / (width - 1);
|
|
60
|
+
h = val;
|
|
61
|
+
c = Math.max(0, Math.min(values.c, 100));
|
|
62
|
+
l = Math.max(0, Math.min(values.l, 100));
|
|
63
|
+
} else if (channel === 'c') {
|
|
64
|
+
const val = (x * 100) / (width - 1);
|
|
65
|
+
h = Math.max(0, Math.min(values.h, 360));
|
|
66
|
+
c = val;
|
|
67
|
+
l = Math.max(0, Math.min(values.l, 100));
|
|
68
|
+
} else {
|
|
69
|
+
// channel === 'l'
|
|
70
|
+
const val = (x * 100) / (width - 1);
|
|
71
|
+
h = Math.max(0, Math.min(values.h, 360));
|
|
72
|
+
c = Math.max(0, Math.min(values.c, 100));
|
|
73
|
+
l = val;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Determine display color
|
|
77
|
+
let displayColor;
|
|
78
|
+
if (hclOk(h, c, l)) {
|
|
79
|
+
const hclColor = hcl(h, c, l);
|
|
80
|
+
const rgbColor = hclColor.rgb();
|
|
81
|
+
displayColor = {
|
|
82
|
+
r: rgbColor.r,
|
|
83
|
+
g: rgbColor.g,
|
|
84
|
+
b: rgbColor.b,
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
// Out-of-gamut: show gray
|
|
88
|
+
displayColor = { r: 128, g: 128, b: 128 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fill the column
|
|
92
|
+
for (let y = 0; y < height; y++) {
|
|
93
|
+
const i = 4 * (y * width + x);
|
|
94
|
+
imageData.data[i] = displayColor.r;
|
|
95
|
+
imageData.data[i + 1] = displayColor.g;
|
|
96
|
+
imageData.data[i + 2] = displayColor.b;
|
|
97
|
+
imageData.data[i + 3] = 255; // Alpha
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ctx.putImageData(imageData, 0, 0);
|
|
102
|
+
};
|
|
@@ -1064,9 +1064,20 @@
|
|
|
1064
1064
|
align-items: center;
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
|
+
.slider-bg-canvas {
|
|
1068
|
+
position: absolute;
|
|
1069
|
+
top: 50%;
|
|
1070
|
+
left: 0;
|
|
1071
|
+
transform: translateY(-50%);
|
|
1072
|
+
width: 100%;
|
|
1073
|
+
height: 12px;
|
|
1074
|
+
border-radius: 6px;
|
|
1075
|
+
pointer-events: none;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1067
1078
|
.slider-input {
|
|
1068
1079
|
width: 100%;
|
|
1069
|
-
height:
|
|
1080
|
+
height: 8px;
|
|
1070
1081
|
background: transparent;
|
|
1071
1082
|
outline: none;
|
|
1072
1083
|
border: none;
|
|
@@ -1081,17 +1092,17 @@
|
|
|
1081
1092
|
// Custom track
|
|
1082
1093
|
&::-webkit-slider-track {
|
|
1083
1094
|
width: 100%;
|
|
1084
|
-
height:
|
|
1095
|
+
height: 8px;
|
|
1085
1096
|
background: var(--vh-color-neutral-02);
|
|
1086
|
-
border-radius:
|
|
1097
|
+
border-radius: 4px;
|
|
1087
1098
|
border: none;
|
|
1088
1099
|
}
|
|
1089
1100
|
|
|
1090
1101
|
&::-moz-range-track {
|
|
1091
1102
|
width: 100%;
|
|
1092
|
-
height:
|
|
1103
|
+
height: 8px;
|
|
1093
1104
|
background: var(--vh-color-neutral-02);
|
|
1094
|
-
border-radius:
|
|
1105
|
+
border-radius: 4px;
|
|
1095
1106
|
border: none;
|
|
1096
1107
|
}
|
|
1097
1108
|
|
|
@@ -1099,8 +1110,8 @@
|
|
|
1099
1110
|
&::-webkit-slider-thumb {
|
|
1100
1111
|
-webkit-appearance: none;
|
|
1101
1112
|
appearance: none;
|
|
1102
|
-
width:
|
|
1103
|
-
height:
|
|
1113
|
+
width: 18px;
|
|
1114
|
+
height: 18px;
|
|
1104
1115
|
background: linear-gradient(
|
|
1105
1116
|
135deg,
|
|
1106
1117
|
#66ecff,
|
|
@@ -1108,26 +1119,26 @@
|
|
|
1108
1119
|
);
|
|
1109
1120
|
border-radius: 50%;
|
|
1110
1121
|
cursor: pointer;
|
|
1111
|
-
border:
|
|
1112
|
-
box-shadow: 0
|
|
1122
|
+
border: 2px solid var(--vh-color-neutral-01);
|
|
1123
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
1113
1124
|
transition: all 0.2s ease;
|
|
1114
1125
|
position: relative;
|
|
1115
1126
|
z-index: 3;
|
|
1116
1127
|
|
|
1117
1128
|
&:hover {
|
|
1118
1129
|
transform: scale(1.1);
|
|
1119
|
-
box-shadow: 0
|
|
1130
|
+
box-shadow: 0 3px 8px rgba(102, 236, 255, 0.4);
|
|
1120
1131
|
}
|
|
1121
1132
|
|
|
1122
1133
|
&:active {
|
|
1123
1134
|
transform: scale(1.05);
|
|
1124
|
-
box-shadow: 0
|
|
1135
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
|
1125
1136
|
}
|
|
1126
1137
|
}
|
|
1127
1138
|
|
|
1128
1139
|
&::-moz-range-thumb {
|
|
1129
|
-
width:
|
|
1130
|
-
height:
|
|
1140
|
+
width: 18px;
|
|
1141
|
+
height: 18px;
|
|
1131
1142
|
background: linear-gradient(
|
|
1132
1143
|
135deg,
|
|
1133
1144
|
#66ecff,
|
|
@@ -1135,18 +1146,18 @@
|
|
|
1135
1146
|
);
|
|
1136
1147
|
border-radius: 50%;
|
|
1137
1148
|
cursor: pointer;
|
|
1138
|
-
border:
|
|
1139
|
-
box-shadow: 0
|
|
1149
|
+
border: 2px solid var(--vh-color-neutral-01);
|
|
1150
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
1140
1151
|
transition: all 0.2s ease;
|
|
1141
1152
|
|
|
1142
1153
|
&:hover {
|
|
1143
1154
|
transform: scale(1.1);
|
|
1144
|
-
box-shadow: 0
|
|
1155
|
+
box-shadow: 0 3px 8px rgba(102, 236, 255, 0.4);
|
|
1145
1156
|
}
|
|
1146
1157
|
|
|
1147
1158
|
&:active {
|
|
1148
1159
|
transform: scale(1.05);
|
|
1149
|
-
box-shadow: 0
|
|
1160
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
|
1150
1161
|
}
|
|
1151
1162
|
}
|
|
1152
1163
|
|
|
@@ -1155,11 +1166,11 @@
|
|
|
1155
1166
|
outline: none;
|
|
1156
1167
|
|
|
1157
1168
|
&::-webkit-slider-thumb {
|
|
1158
|
-
box-shadow: 0 0 0
|
|
1169
|
+
box-shadow: 0 0 0 3px rgba(102, 236, 255, 0.3);
|
|
1159
1170
|
}
|
|
1160
1171
|
|
|
1161
1172
|
&::-moz-range-thumb {
|
|
1162
|
-
box-shadow: 0 0 0
|
|
1173
|
+
box-shadow: 0 0 0 3px rgba(102, 236, 255, 0.3);
|
|
1163
1174
|
}
|
|
1164
1175
|
}
|
|
1165
1176
|
}
|
|
@@ -1168,9 +1179,9 @@
|
|
|
1168
1179
|
position: absolute;
|
|
1169
1180
|
top: 50%;
|
|
1170
1181
|
left: 0;
|
|
1171
|
-
height:
|
|
1182
|
+
height: 8px;
|
|
1172
1183
|
background: linear-gradient(90deg, #77fd8c, #66ecff);
|
|
1173
|
-
border-radius:
|
|
1184
|
+
border-radius: 4px;
|
|
1174
1185
|
transform: translateY(-50%);
|
|
1175
1186
|
transition: width 0.1s ease;
|
|
1176
1187
|
pointer-events: none;
|