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.
@@ -5,35 +5,17 @@ import {
5
5
  useEffect,
6
6
  useMemo,
7
7
  } from 'react';
8
- import { VZCodeContext } from '../VZCodeContext';
8
+ import { VZCodeContext } from '../../VZCodeContext';
9
9
  import { VizContent, VizFileId } from '@vizhub/viz-types';
10
- import { VisualEditorConfigEntry } from '../../types';
11
- import { EmptyState } from './EmptyState';
10
+ import { VisualEditorConfigEntry } from '../../../types';
11
+ import { EmptyState } from '../EmptyState';
12
12
  import { color, hcl, HCLColor, rgb } from 'd3-color';
13
-
14
- const CONFIG_FILE_NAME = 'config.json';
15
-
16
- // Helper function to calculate percentage for progress fill
17
- const calculatePercentage = (
18
- value: number,
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
- <div
405
+ <SliderWidget
429
406
  key={widgetConfig.property}
430
- className="visual-editor-slider"
431
- >
432
- <div className="slider-header">
433
- <label
434
- htmlFor={widgetConfig.property}
435
- className="slider-label"
436
- >
437
- {widgetConfig.label}
438
- </label>
439
- <span className="slider-value">
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
- <div
425
+ <CheckboxWidget
483
426
  key={widgetConfig.property}
484
- className="visual-editor-checkbox"
485
- >
486
- <div className="checkbox-header">
487
- <label
488
- htmlFor={widgetConfig.property}
489
- className="checkbox-label"
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
- <div
442
+ <TextInputWidget
525
443
  key={widgetConfig.property}
526
- className="visual-editor-text-input"
527
- >
528
- <div className="text-input-header">
529
- <label
530
- htmlFor={widgetConfig.property}
531
- className="text-input-label"
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
- <div
461
+ <DropdownWidget
559
462
  key={widgetConfig.property}
560
- className="visual-editor-dropdown"
561
- >
562
- <div className="dropdown-header">
563
- <label
564
- htmlFor={widgetConfig.property}
565
- className="dropdown-label"
566
- >
567
- {widgetConfig.label}
568
- </label>
569
- {/* <span className="dropdown-value">
570
- {currentValue}
571
- </span> */}
572
- </div>
573
- <div className="dropdown-container">
574
- <button
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
- <div
507
+ <ColorWidget
667
508
  key={widgetConfig.property}
668
- className="visual-editor-color"
669
- >
670
- <div className="color-header">
671
- <label className="color-label">
672
- {widgetConfig.label}
673
- </label>
674
- <span className="color-value">
675
- {currentHex}
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: 4px;
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: 4px;
1095
+ height: 8px;
1085
1096
  background: var(--vh-color-neutral-02);
1086
- border-radius: 2px;
1097
+ border-radius: 4px;
1087
1098
  border: none;
1088
1099
  }
1089
1100
 
1090
1101
  &::-moz-range-track {
1091
1102
  width: 100%;
1092
- height: 4px;
1103
+ height: 8px;
1093
1104
  background: var(--vh-color-neutral-02);
1094
- border-radius: 2px;
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: 14px;
1103
- height: 14px;
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: 1px solid var(--vh-color-neutral-01);
1112
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
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 2px 4px rgba(102, 236, 255, 0.4);
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 1px 2px rgba(0, 0, 0, 0.4);
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: 14px;
1130
- height: 14px;
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: 1px solid var(--vh-color-neutral-01);
1139
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
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 2px 4px rgba(102, 236, 255, 0.4);
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 1px 2px rgba(0, 0, 0, 0.4);
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 2px rgba(102, 236, 255, 0.3);
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 2px rgba(102, 236, 255, 0.3);
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: 4px;
1182
+ height: 8px;
1172
1183
  background: linear-gradient(90deg, #77fd8c, #66ecff);
1173
- border-radius: 2px;
1184
+ border-radius: 4px;
1174
1185
  transform: translateY(-50%);
1175
1186
  transition: width 0.1s ease;
1176
1187
  pointer-events: none;