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.
- package/dist/assets/index-858eZSOF.js +457 -0
- package/dist/assets/{index-Bjj9VRMn.css → index-DLm5FQ0E.css} +1 -1
- package/dist/assets/{worker-DHxSPM7N.js → worker-CHvi_bXc.js} +13 -13
- package/dist/index.html +2 -2
- package/package.json +26 -24
- package/src/client/VZSidebar/AIChat/Message.tsx +4 -1
- package/src/client/VZSidebar/AIChat/MessageList.tsx +13 -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} +202 -211
- 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/index.tsx +0 -23
- package/src/client/VZSidebar/styles.scss +246 -0
- package/src/types.ts +2 -1
- package/dist/assets/index-CsDf6ybm.js +0 -457
|
@@ -5,34 +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 '
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
<
|
|
405
|
+
<SliderWidget
|
|
318
406
|
key={widgetConfig.property}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
<
|
|
425
|
+
<CheckboxWidget
|
|
372
426
|
key={widgetConfig.property}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
<
|
|
442
|
+
<TextInputWidget
|
|
414
443
|
key={widgetConfig.property}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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={
|