vzcode 2.10.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,34 +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';
12
-
13
- const CONFIG_FILE_NAME = 'config.json';
14
-
15
- // Helper function to calculate percentage for progress fill
16
- const calculatePercentage = (
17
- value: number,
18
- min: number,
19
- max: number,
20
- ) => {
21
- return ((value - min) / (max - min)) * 100;
22
- };
23
-
24
- // Helper function to format values nicely
25
- const formatValue = (
26
- value: number,
27
- min: number,
28
- max: number,
29
- ) => {
30
- // Determine decimal places based on the range
31
- const range = max - min;
32
- const decimalPlaces =
33
- range < 10 ? 2 : range < 100 ? 1 : 0;
34
- return Number(value).toFixed(decimalPlaces);
35
- };
10
+ import { VisualEditorConfigEntry } from '../../../types';
11
+ import { EmptyState } from '../EmptyState';
12
+ import { color, hcl, HCLColor, rgb } from 'd3-color';
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';
36
19
 
37
20
  export const VisualEditor = () => {
38
21
  const { files, submitOperation } =
@@ -229,6 +212,90 @@ export const VisualEditor = () => {
229
212
  };
230
213
  }, [openDropdown]);
231
214
 
215
+ const onColorChange = useCallback(
216
+ (property: string, lchComponent: 'l' | 'c' | 'h') =>
217
+ (event: React.ChangeEvent<HTMLInputElement>) => {
218
+ const newValue = parseFloat(
219
+ event.currentTarget.value,
220
+ );
221
+
222
+ // Get current hex color from config
223
+ const currentHex =
224
+ configData[property] || '#000000';
225
+
226
+ // Convert current hex to LCH
227
+ let hclFromRGB: HCLColor;
228
+ try {
229
+ const rgbColor = rgb(currentHex);
230
+ hclFromRGB = hcl(rgbColor);
231
+ } catch (error) {
232
+ // Fallback to black if conversion fails
233
+ hclFromRGB = hcl('black');
234
+ }
235
+
236
+ let hclArray: number[] = [
237
+ (localValues[`${property}_h`] as number) ??
238
+ hclFromRGB.h,
239
+ (localValues[`${property}_c`] as number) ??
240
+ hclFromRGB.c,
241
+ (localValues[`${property}_l`] as number) ??
242
+ hclFromRGB.l,
243
+ ];
244
+
245
+ // Update the specific LCH component
246
+ const newhcl = [...hclArray];
247
+ if (lchComponent === 'h') newhcl[0] = newValue;
248
+ else if (lchComponent === 'c') newhcl[1] = newValue;
249
+ else if (lchComponent === 'l') newhcl[2] = newValue;
250
+
251
+ // Convert back to hex
252
+ let newHex;
253
+ try {
254
+ const newColor = hcl(
255
+ newhcl[0],
256
+ newhcl[1],
257
+ newhcl[2],
258
+ );
259
+
260
+ if (newColor.displayable()) {
261
+ newHex = newColor.formatHex();
262
+ } else {
263
+ newHex = currentHex;
264
+ }
265
+ } catch (error) {
266
+ // Fallback to current color if conversion fails
267
+ newHex = currentHex;
268
+ }
269
+
270
+ // Update local state for responsive UI
271
+ setLocalValues((prev) => ({
272
+ ...prev,
273
+ [property]: newHex,
274
+ [`${property}_h`]: newhcl[0],
275
+ [`${property}_c`]: newhcl[1],
276
+ [`${property}_l`]: newhcl[2],
277
+ }));
278
+
279
+ // Update config.json with hex value
280
+ const newConfigData = {
281
+ ...configData,
282
+ [property]: newHex,
283
+ };
284
+
285
+ submitOperation((document: VizContent) => ({
286
+ ...document,
287
+ files: {
288
+ ...files,
289
+ [configFileId]: {
290
+ name: 'config.json',
291
+ text: JSON.stringify(newConfigData, null, 2),
292
+ },
293
+ },
294
+ }));
295
+ },
296
+ [configData, files, configFileId, setLocalValues],
297
+ );
298
+
232
299
  const visualEditorWidgets: VisualEditorConfigEntry[] =
233
300
  configData?.visualEditorWidgets ?? [];
234
301
 
@@ -250,6 +317,32 @@ export const VisualEditor = () => {
250
317
  } else if (widget.type === 'dropdown') {
251
318
  newLocalValues[widget.property] =
252
319
  configData[widget.property];
320
+ } else if (widget.type === 'color') {
321
+ newLocalValues[widget.property] =
322
+ configData[widget.property];
323
+
324
+ // Convert hex to LCH for internal state
325
+ const hexColor = configData[widget.property];
326
+
327
+ if (localValues[widget.property] !== hexColor) {
328
+ if (hexColor) {
329
+ try {
330
+ const rgbColor = color(hexColor);
331
+ const lch = hcl(rgbColor);
332
+ newLocalValues[`${widget.property}_h`] =
333
+ lch.h;
334
+ newLocalValues[`${widget.property}_c`] =
335
+ lch.c;
336
+ newLocalValues[`${widget.property}_l`] =
337
+ lch.l;
338
+ } catch (error) {
339
+ // Fallback values if conversion fails
340
+ newLocalValues[`${widget.property}_l`] = 0;
341
+ newLocalValues[`${widget.property}_c`] = 0;
342
+ newLocalValues[`${widget.property}_h`] = 0;
343
+ }
344
+ }
345
+ }
253
346
  }
254
347
  });
255
348
  setLocalValues(newLocalValues);
@@ -307,59 +400,20 @@ export const VisualEditor = () => {
307
400
  const currentValue =
308
401
  localValues[widgetConfig.property] ??
309
402
  configData[widgetConfig.property];
310
- const percentage = calculatePercentage(
311
- currentValue,
312
- widgetConfig.min,
313
- widgetConfig.max,
314
- );
315
403
 
316
404
  return (
317
- <div
405
+ <SliderWidget
318
406
  key={widgetConfig.property}
319
- className="visual-editor-slider"
320
- >
321
- <div className="slider-header">
322
- <label
323
- htmlFor={widgetConfig.property}
324
- className="slider-label"
325
- >
326
- {widgetConfig.label}
327
- </label>
328
- <span className="slider-value">
329
- {formatValue(
330
- currentValue,
331
- widgetConfig.min,
332
- widgetConfig.max,
333
- )}
334
- </span>
335
- </div>
336
- <div className="slider-container">
337
- <input
338
- type="range"
339
- id={widgetConfig.property}
340
- className="slider-input"
341
- min={widgetConfig.min}
342
- max={widgetConfig.max}
343
- step={widgetConfig.step}
344
- value={currentValue}
345
- onChange={onSliderChange(
346
- widgetConfig.property,
347
- )}
348
- />
349
- <div
350
- className="slider-track-fill"
351
- style={{ width: `${percentage}%` }}
352
- />
353
- </div>
354
- <div className="slider-bounds">
355
- <span className="min-value">
356
- {widgetConfig.min}
357
- </span>
358
- <span className="max-value">
359
- {widgetConfig.max}
360
- </span>
361
- </div>
362
- </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
+ />
363
417
  );
364
418
  } else if (widgetConfig.type === 'checkbox') {
365
419
  // Use local value if available, otherwise fall back to config value
@@ -368,40 +422,15 @@ export const VisualEditor = () => {
368
422
  configData[widgetConfig.property];
369
423
 
370
424
  return (
371
- <div
425
+ <CheckboxWidget
372
426
  key={widgetConfig.property}
373
- className="visual-editor-checkbox"
374
- >
375
- <div className="checkbox-header">
376
- <label
377
- htmlFor={widgetConfig.property}
378
- className="checkbox-label"
379
- >
380
- {widgetConfig.label}
381
- </label>
382
- <span className="checkbox-value">
383
- {currentValue ? 'On' : 'Off'}
384
- </span>
385
- </div>
386
- <div className="checkbox-container">
387
- <input
388
- type="checkbox"
389
- id={widgetConfig.property}
390
- className="checkbox-input"
391
- checked={currentValue}
392
- onChange={onCheckboxChange(
393
- widgetConfig.property,
394
- )}
395
- />
396
- <div className="checkbox-visual">
397
- <div
398
- className={`checkbox-indicator ${
399
- currentValue ? 'checked' : ''
400
- }`}
401
- />
402
- </div>
403
- </div>
404
- </div>
427
+ property={widgetConfig.property}
428
+ label={widgetConfig.label}
429
+ currentValue={currentValue}
430
+ onChange={onCheckboxChange(
431
+ widgetConfig.property,
432
+ )}
433
+ />
405
434
  );
406
435
  } else if (widgetConfig.type === 'textInput') {
407
436
  // Use local value if available, otherwise fall back to config value
@@ -410,30 +439,15 @@ export const VisualEditor = () => {
410
439
  configData[widgetConfig.property];
411
440
 
412
441
  return (
413
- <div
442
+ <TextInputWidget
414
443
  key={widgetConfig.property}
415
- className="visual-editor-text-input"
416
- >
417
- <div className="text-input-header">
418
- <label
419
- htmlFor={widgetConfig.property}
420
- className="text-input-label"
421
- >
422
- {widgetConfig.label}
423
- </label>
424
- </div>
425
- <div className="text-input-container">
426
- <input
427
- type="text"
428
- id={widgetConfig.property}
429
- className="text-input-field"
430
- value={currentValue || ''}
431
- onChange={onTextInputChange(
432
- widgetConfig.property,
433
- )}
434
- />
435
- </div>
436
- </div>
444
+ property={widgetConfig.property}
445
+ label={widgetConfig.label}
446
+ currentValue={currentValue}
447
+ onChange={onTextInputChange(
448
+ widgetConfig.property,
449
+ )}
450
+ />
437
451
  );
438
452
  } else if (widgetConfig.type === 'dropdown') {
439
453
  // Use local value if available, otherwise fall back to config value
@@ -444,85 +458,62 @@ export const VisualEditor = () => {
444
458
  openDropdown === widgetConfig.property;
445
459
 
446
460
  return (
447
- <div
461
+ <DropdownWidget
462
+ key={widgetConfig.property}
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
+ />
478
+ );
479
+ } else if (widgetConfig.type === 'color') {
480
+ // Use local value if available, otherwise fall back to config value
481
+ const currentHex =
482
+ localValues[widgetConfig.property] ??
483
+ configData[widgetConfig.property] ??
484
+ '#000000';
485
+
486
+ // Convert hex to LCH for slider values
487
+ let hclColor;
488
+ try {
489
+ const rgbColor = color(currentHex);
490
+ hclColor = hcl(rgbColor);
491
+ } catch (error) {
492
+ hclColor = hcl('black');
493
+ }
494
+
495
+ // Use local LCH values if available, otherwise convert from hex
496
+ const currentH =
497
+ localValues[`${widgetConfig.property}_h`] ??
498
+ hclColor.h;
499
+ const currentC =
500
+ localValues[`${widgetConfig.property}_c`] ??
501
+ hclColor.c;
502
+ const currentL =
503
+ localValues[`${widgetConfig.property}_l`] ??
504
+ hclColor.l;
505
+
506
+ return (
507
+ <ColorWidget
448
508
  key={widgetConfig.property}
449
- className="visual-editor-dropdown"
450
- >
451
- <div className="dropdown-header">
452
- <label
453
- htmlFor={widgetConfig.property}
454
- className="dropdown-label"
455
- >
456
- {widgetConfig.label}
457
- </label>
458
- {/* <span className="dropdown-value">
459
- {currentValue}
460
- </span> */}
461
- </div>
462
- <div className="dropdown-container">
463
- <button
464
- type="button"
465
- className={`dropdown-button ${
466
- isOpen ? 'open' : ''
467
- }`}
468
- onClick={() =>
469
- handleDropdownToggle(
470
- widgetConfig.property,
471
- )
472
- }
473
- aria-expanded={isOpen}
474
- aria-haspopup="listbox"
475
- >
476
- <span className="dropdown-button-text">
477
- {currentValue}
478
- </span>
479
- <div
480
- className={`dropdown-arrow ${
481
- isOpen ? 'open' : ''
482
- }`}
483
- >
484
- <svg
485
- width="12"
486
- height="8"
487
- viewBox="0 0 12 8"
488
- fill="none"
489
- xmlns="http://www.w3.org/2000/svg"
490
- >
491
- <path
492
- d="M1 1.5L6 6.5L11 1.5"
493
- stroke="currentColor"
494
- strokeWidth="2"
495
- strokeLinecap="round"
496
- strokeLinejoin="round"
497
- />
498
- </svg>
499
- </div>
500
- </button>
501
- {isOpen && (
502
- <div className="dropdown-options">
503
- {widgetConfig.options.map((option) => (
504
- <button
505
- key={option}
506
- type="button"
507
- className={`dropdown-option ${
508
- option === currentValue
509
- ? 'selected'
510
- : ''
511
- }`}
512
- onClick={() =>
513
- handleDropdownOptionClick(
514
- widgetConfig.property,
515
- option,
516
- )
517
- }
518
- >
519
- {option}
520
- </button>
521
- ))}
522
- </div>
523
- )}
524
- </div>
525
- </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
+ />
526
517
  );
527
518
  }
528
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
+ };
@@ -396,29 +396,6 @@ export const VZSidebar = ({
396
396
  </i>
397
397
  </OverlayTrigger>
398
398
 
399
- {enableAIChat && (
400
- <OverlayTrigger
401
- placement="right"
402
- overlay={
403
- <Tooltip id="ai-chat-tooltip">
404
- {aiChatToolTipText}
405
- </Tooltip>
406
- }
407
- >
408
- <i
409
- id="ai-chat-icon"
410
- className="icon-button icon-button-dark"
411
- onClick={() => {
412
- setIsAIChatOpen(true);
413
- setIsSearchOpen(false);
414
- setIsVisualEditorOpen(false);
415
- }}
416
- >
417
- <SparklesSVG />
418
- </i>
419
- </OverlayTrigger>
420
- )}
421
-
422
399
  <OverlayTrigger
423
400
  placement="right"
424
401
  overlay={