fragment-tools 0.2.12 → 0.2.13

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.
Files changed (54) hide show
  1. package/package.json +12 -11
  2. package/src/cli/build.js +1 -0
  3. package/src/cli/create.js +22 -4
  4. package/src/cli/createConfig.js +2 -2
  5. package/src/cli/getEntries.js +10 -1
  6. package/src/cli/plugins/hot-shader-replacement.js +54 -16
  7. package/src/cli/plugins/save.js +97 -38
  8. package/src/cli/prompts.js +89 -36
  9. package/src/cli/run.js +1 -1
  10. package/src/client/app/actions/resize.js +8 -1
  11. package/src/client/app/attachments/draggable.js +93 -0
  12. package/src/client/app/client.js +90 -18
  13. package/src/client/app/components/IconFlip.svelte +46 -0
  14. package/src/client/app/hooks.js +25 -1
  15. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +95 -3
  16. package/src/client/app/lib/canvas-recorder/FrameRecorder.js +45 -3
  17. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +72 -13
  18. package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +43 -9
  19. package/src/client/app/lib/canvas-recorder/utils.js +18 -9
  20. package/src/client/app/renderers/2DRenderer.js +20 -16
  21. package/src/client/app/renderers/P5GLRenderer.js +13 -5
  22. package/src/client/app/renderers/P5Renderer.js +9 -1
  23. package/src/client/app/renderers/THREERenderer.js +61 -47
  24. package/src/client/app/state/Sketch.svelte.js +149 -9
  25. package/src/client/app/state/errors.svelte.js +19 -0
  26. package/src/client/app/state/exports.svelte.js +14 -1
  27. package/src/client/app/state/rendering.svelte.js +47 -0
  28. package/src/client/app/state/sketches.svelte.js +43 -7
  29. package/src/client/app/state/utils.svelte.js +49 -0
  30. package/src/client/app/ui/Field.svelte +6 -1
  31. package/src/client/app/ui/FieldSection.svelte +4 -4
  32. package/src/client/app/ui/ParamsOutput.svelte +1 -1
  33. package/src/client/app/ui/SketchRenderer.svelte +16 -0
  34. package/src/client/app/ui/fields/ButtonInput.svelte +2 -0
  35. package/src/client/app/ui/fields/CheckboxInput.svelte +13 -11
  36. package/src/client/app/ui/fields/ColorInput.svelte +16 -11
  37. package/src/client/app/ui/fields/GradientInput.svelte +607 -0
  38. package/src/client/app/ui/fields/Input.svelte +10 -6
  39. package/src/client/app/ui/fields/IntervalInput.svelte +27 -35
  40. package/src/client/app/ui/fields/NumberInput.svelte +51 -13
  41. package/src/client/app/ui/fields/PaletteInput.svelte +181 -0
  42. package/src/client/app/ui/fields/ProgressInput.svelte +44 -16
  43. package/src/client/app/ui/fields/TextareaInput.svelte +10 -10
  44. package/src/client/app/utils/canvas.utils.js +105 -28
  45. package/src/client/app/utils/color.utils.js +74 -17
  46. package/src/client/app/utils/fields.utils.js +68 -26
  47. package/src/client/app/utils/file.utils.js +86 -31
  48. package/src/client/app/utils/glsl.utils.js +11 -2
  49. package/src/client/app/utils/glslErrors.js +31 -21
  50. package/src/client/app/utils/index.js +28 -12
  51. package/src/client/main.js +7 -1
  52. package/src/types/global.d.ts +143 -0
  53. package/src/types/props.d.ts +40 -15
  54. package/tsconfig.json +1 -1
@@ -10,7 +10,63 @@ import { exportCanvas } from '../lib/canvas-recorder/utils';
10
10
  import { map } from './math.utils';
11
11
  import { createDataURLFromBlob, saveFiles } from './file.utils';
12
12
 
13
- function getFilenameParams() {
13
+ /**
14
+ * @typedef {Object} FilenameParams
15
+ * @property {string} year - Four-digit year
16
+ * @property {string} month - Two-digit month
17
+ * @property {string} day - Two-digit day
18
+ * @property {string} hours - Two-digit hours
19
+ * @property {string} minutes - Two-digit minutes
20
+ * @property {string} seconds - Two-digit seconds
21
+ * @property {string} timestamp - Full timestamp string
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} ScreenshotOptions
26
+ * @property {string} [filename='Screenshot'] - Base filename
27
+ * @property {number} [index] - Optional frame index
28
+ * @property {FilenamePattern} [pattern] - Filename pattern function
29
+ * @property {string} [exportDir] - Export directory path
30
+ * @property {Record<string, any>} [params={}] - Additional parameters for pattern
31
+ * @property {string} [encoding='png'] - Image encoding format
32
+ * @property {number} [quality=100] - Image quality (1-100)
33
+ * @property {number} [pixelsPerInch=72] - Image DPI
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} RecordCanvasOptions
38
+ * @property {string} [filename='output'] - Base filename for output
39
+ * @property {string} [format='mp4'] - Output format
40
+ * @property {number} [framerate=25] - Frames per second
41
+ * @property {number} [duration=Infinity] - Recording duration in seconds
42
+ * @property {number} [quality=100] - Recording quality (1-100)
43
+ * @property {FilenamePattern} [pattern] - Filename pattern function
44
+ * @property {import('../lib/canvas-recorder/MediaBunnyRecorder').VideoCodec} [codec] - Video codec
45
+ * @property {string} [exportDir] - Export directory path
46
+ * @property {string} [imageEncoding] - Image encoding for frame exports
47
+ * @property {import('../lib/canvas-recorder/CanvasRecorder').CanvasRecorderStartCallback} [onStart] - Callback when recording starts
48
+ * @property {import('../lib/canvas-recorder/CanvasRecorder').CanvasRecorderTickCallback} [onTick] - Callback on each frame
49
+ * @property {import('../lib/canvas-recorder/CanvasRecorder').CanvasRecorderCompleteCallback} [onComplete] - Callback when recording completes
50
+ * @property {Record<string, any>} [params={}] - Additional parameters
51
+ */
52
+ /**
53
+ * @typedef {Object} PatternParams
54
+ * @property {number} [index] - Frame or sequence index
55
+ * @property {string} filename - Base filename
56
+ * @property {string} timestamp - Timestamp string
57
+ */
58
+
59
+ /**
60
+ * @callback FilenamePattern
61
+ * @param {PatternParams & Record<string, any>} params - Pattern parameters
62
+ * @returns {string} Generated filename
63
+ */
64
+
65
+ /**
66
+ * Get current date/time parameters for filename generation
67
+ * @returns {FilenameParams}
68
+ */
69
+ export function getFilenameParams() {
14
70
  const now = new Date();
15
71
 
16
72
  const year = now.toLocaleString('default', { year: 'numeric' });
@@ -43,10 +99,14 @@ function getFilenameParams() {
43
99
  };
44
100
  }
45
101
 
102
+ /**
103
+ * Default filename pattern generator
104
+ * @type {FilenamePattern}
105
+ */
46
106
  export const defaultFilenamePattern = ({ index, filename, timestamp }) => {
47
107
  let name = `${filename}.${timestamp}`;
48
108
 
49
- if (!isNaN(index)) {
109
+ if (typeof index === 'number' && !isNaN(index)) {
50
110
  name += `-${index}`;
51
111
  }
52
112
 
@@ -54,14 +114,12 @@ export const defaultFilenamePattern = ({ index, filename, timestamp }) => {
54
114
  };
55
115
 
56
116
  /**
57
- *
58
- * @param {HTMLCanvasElement} canvas
59
- * @param {string} sketchKey
60
- * @param {Sketch} sketch
61
- * @param {number} [index]
62
- * @param {Promise<string[]>}
117
+ * Capture and save a screenshot of a canvas
118
+ * @param {HTMLCanvasElement} canvas - The canvas to capture
119
+ * @param {ScreenshotOptions} [options={}] - Screenshot options
120
+ * @returns {{ filename: string, exportDir: string | undefined, data: string, encoding: string }}
63
121
  */
64
- export async function screenshotCanvas(
122
+ export function screenshotCanvas(
65
123
  canvas,
66
124
  {
67
125
  filename = 'Screenshot',
@@ -83,23 +141,20 @@ export async function screenshotCanvas(
83
141
  let patternParams = getFilenameParams();
84
142
  let name = pattern({ filename, index, ...params, ...patternParams });
85
143
 
86
- const files = [
87
- {
88
- filename: `${name}${extension}`,
89
- exportDir,
90
- data: dataURL,
91
- encoding: 'base64',
92
- },
93
- ];
94
-
95
- try {
96
- await saveFiles(files);
97
- } catch (error) {
98
- console.error(`[fragment] Error while saving screenshot.`);
99
- console.log(error);
100
- }
144
+ return {
145
+ filename: `${name}${extension}`,
146
+ exportDir,
147
+ data: dataURL,
148
+ encoding: 'base64',
149
+ };
101
150
  }
102
151
 
152
+ /**
153
+ * Record video using MediaBunny recorder
154
+ * @param {HTMLCanvasElement} canvas - The canvas to record
155
+ * @param {import('../lib/canvas-recorder/MediaBunnyRecorder').MediaBunnyRecorderOptions} options - Recording options
156
+ * @returns {MediaBunnyRecorder}
157
+ */
103
158
  function record(canvas, options) {
104
159
  let recorder = new MediaBunnyRecorder(canvas, options);
105
160
  recorder.start();
@@ -107,6 +162,12 @@ function record(canvas, options) {
107
162
  return recorder;
108
163
  }
109
164
 
165
+ /**
166
+ * Record GIF animation
167
+ * @param {HTMLCanvasElement} canvas - The canvas to record
168
+ * @param {import('../lib/canvas-recorder/CanvasRecorder').CanvasRecorderOptions} options - Recording options
169
+ * @returns {GIFRecorder}
170
+ */
110
171
  function recordGIF(canvas, options) {
111
172
  let recorder = new GIFRecorder(canvas, options);
112
173
  recorder.start();
@@ -114,6 +175,12 @@ function recordGIF(canvas, options) {
114
175
  return recorder;
115
176
  }
116
177
 
178
+ /**
179
+ * Record individual frames
180
+ * @param {HTMLCanvasElement} canvas - The canvas to record
181
+ * @param {import('../lib/canvas-recorder/FrameRecorder').FrameRecorderOptions} options - Recording options
182
+ * @returns {FrameRecorder}
183
+ */
117
184
  function recordFrames(canvas, options) {
118
185
  let recorder = new FrameRecorder(canvas, options);
119
186
  recorder.start();
@@ -121,6 +188,12 @@ function recordFrames(canvas, options) {
121
188
  return recorder;
122
189
  }
123
190
 
191
+ /**
192
+ * Start recording canvas output
193
+ * @param {HTMLCanvasElement} canvas - The canvas to record
194
+ * @param {RecordCanvasOptions} [options={}] - Recording options
195
+ * @returns {MediaBunnyRecorder | GIFRecorder | FrameRecorder | undefined}
196
+ */
124
197
  export function recordCanvas(
125
198
  canvas,
126
199
  {
@@ -130,18 +203,22 @@ export function recordCanvas(
130
203
  duration = Infinity,
131
204
  quality = 100,
132
205
  pattern = defaultFilenamePattern,
133
- codec,
206
+ codec = 'avc',
134
207
  exportDir,
135
208
  imageEncoding,
136
209
  onStart = () => {},
137
210
  onTick = () => {},
138
211
  onComplete = () => {},
139
- params = {},
140
212
  } = {},
141
213
  ) {
142
214
  let patternParams = getFilenameParams();
143
215
  let name = pattern({ filename, ...patternParams });
144
216
 
217
+ /**
218
+ * Handle recording completion
219
+ * @param {Blob | Blob[] | null} result - Recording result (blob or array of blobs)
220
+ * @returns {Promise<void>}
221
+ */
145
222
  async function complete(result) {
146
223
  const files = [];
147
224
 
@@ -163,7 +240,7 @@ export function recordCanvas(
163
240
  encoding: 'base64',
164
241
  });
165
242
  }
166
- } else {
243
+ } else if (result) {
167
244
  const blob = result;
168
245
  const data = await createDataURLFromBlob(blob);
169
246
 
@@ -177,7 +254,7 @@ export function recordCanvas(
177
254
  }
178
255
 
179
256
  await saveFiles(files);
180
- onComplete();
257
+ onComplete(result);
181
258
  }
182
259
 
183
260
  const options = {
@@ -5,7 +5,9 @@ export const FORMATS = {
5
5
  HSL_STRING: 'hsl-string',
6
6
  HSLA_STRING: 'hsla-string',
7
7
  RGB_OBJECT: 'rgb-object',
8
+ RGB_OBJECT_STRING: 'rgb-object-string',
8
9
  RGBA_OBJECT: 'rgba-object',
10
+ RGBA_OBJECT_STRING: 'rgba-object-string',
9
11
  VEC3_STRING: 'vec3-string',
10
12
  VEC4_STRING: 'vec4-string',
11
13
  VEC3_ARRAY: 'vec3-array',
@@ -32,6 +34,11 @@ export function toHex(color, format = getColorFormat(color)) {
32
34
  if (format === FORMATS.VEC3_ARRAY || format === FORMATS.VEC4_ARRAY)
33
35
  return vecArrayToHex(color);
34
36
  if (format === FORMATS.CSS_COLOR) return nameToHex(color);
37
+ if (
38
+ format === FORMATS.RGB_OBJECT_STRING ||
39
+ format === FORMATS.RGBA_OBJECT_STRING
40
+ )
41
+ return objectStringToHex(color);
35
42
  }
36
43
 
37
44
  export function toComponents(color, format = getColorFormat(color)) {
@@ -50,6 +57,11 @@ export function toComponents(color, format = getColorFormat(color)) {
50
57
  if (format === FORMATS.VEC3_STRING || format === FORMATS.VEC4_STRING)
51
58
  return vecStringToComponents(color);
52
59
  if (format === FORMATS.CSS_COLOR) return nameToComponents(color);
60
+ if (
61
+ format === FORMATS.RGB_OBJECT_STRING ||
62
+ format === FORMATS.RGBA_OBJECT_STRING
63
+ )
64
+ return objectStringToComponents(color);
53
65
  }
54
66
 
55
67
  export function toString(color, format = getColorFormat(color)) {
@@ -61,6 +73,8 @@ export function toString(color, format = getColorFormat(color)) {
61
73
  format === FORMATS.HEX_STRING ||
62
74
  format === FORMATS.RGB_STRING ||
63
75
  format === FORMATS.RGBA_STRING ||
76
+ format === FORMATS.RGB_OBJECT_STRING ||
77
+ format === FORMATS.RGBA_OBJECT_STRING ||
64
78
  format === FORMATS.HSL_STRING ||
65
79
  format === FORMATS.HSLA_STRING ||
66
80
  format === FORMATS.VEC3_STRING ||
@@ -74,9 +88,9 @@ export function toString(color, format = getColorFormat(color)) {
74
88
  return componentsToRGBAString(vecArrayToComponents(color));
75
89
  if (format === FORMATS.THREE) return threeToHex(color);
76
90
  if (format === FORMATS.RGB_OBJECT)
77
- return componentsToRGBStringObject([color.r, color.g, color.b]);
91
+ return componentsToRGBObjectString([color.r, color.g, color.b]);
78
92
  if (format === FORMATS.RGBA_OBJECT)
79
- return componentsToRGBAStringObject([
93
+ return componentsToRGBAObjectString([
80
94
  color.r,
81
95
  color.g,
82
96
  color.b,
@@ -278,12 +292,12 @@ export function toVec3String(color) {
278
292
  return componentsToVec3String(toComponents(color));
279
293
  }
280
294
 
281
- export function toRGBStringObject(color) {
282
- return componentsToRGBStringObject(toComponents(color));
295
+ export function toRGBObjectString(color) {
296
+ return componentsToRGBObjectString(toComponents(color));
283
297
  }
284
298
 
285
- export function toRGBAStringObject(color) {
286
- return componentsToRGBAStringObject(toComponents(color));
299
+ export function toRGBAObjectString(color) {
300
+ return componentsToRGBAObjectString(toComponents(color));
287
301
  }
288
302
 
289
303
  export function toVec4String(color) {
@@ -312,7 +326,19 @@ export function componentsToVec3String(components = []) {
312
326
  return `vec3(${rn}, ${gn}, ${bn})`;
313
327
  }
314
328
 
315
- export function componentsToRGBStringObject(components) {
329
+ export function componentsToRGBObject(components) {
330
+ const [r = 0, g = 0, b = 0] = components;
331
+
332
+ return { r, g, b };
333
+ }
334
+
335
+ export function componentsToRGBAObject(components) {
336
+ const [r = 0, g = 0, b = 0, a = 1] = components;
337
+
338
+ return { r, g, b, a };
339
+ }
340
+
341
+ export function componentsToRGBObjectString(components) {
316
342
  const [r = 0, g = 0, b = 0] = components;
317
343
 
318
344
  let rn = `${Math.round(r * 1000) / 1000}`;
@@ -322,14 +348,15 @@ export function componentsToRGBStringObject(components) {
322
348
  return `{ r: ${rn}, g: ${gn}, b: ${bn} }`;
323
349
  }
324
350
 
325
- export function componentsToRGBAStringObject(components) {
351
+ export function componentsToRGBAObjectString(components) {
326
352
  const [r = 0, g = 0, b = 0, a = 1] = components;
327
353
 
328
354
  let rn = `${Math.round(r * 1000) / 1000}`;
329
355
  let gn = `${Math.round(g * 1000) / 1000}`;
330
356
  let bn = `${Math.round(b * 1000) / 1000}`;
357
+ let an = `${Math.round(a * 1000) / 1000}`;
331
358
 
332
- return `{ r: ${rn}, g: ${gn}, b: ${bn}, a: ${a} }`;
359
+ return `{ r: ${rn}, g: ${gn}, b: ${bn}, a: ${an} }`;
333
360
  }
334
361
 
335
362
  export function componentsToVec4String(components = []) {
@@ -450,6 +477,14 @@ export function componentsToHex(components = []) {
450
477
  return `#${hex}`;
451
478
  }
452
479
 
480
+ export function objectStringToComponents(value) {
481
+ const parsable = value.replace(/([{,]\s*)([a-z]+)(\s*:)/g, '$1"$2"$3');
482
+
483
+ const { r, g, b, a = 1 } = JSON.parse(parsable);
484
+
485
+ return [r, g, b, a];
486
+ }
487
+
453
488
  export function isHexString(value, isString = typeof value === 'string') {
454
489
  return isString && /^#([a-f0-9]{3,4}){1,2}$/i.test(value);
455
490
  }
@@ -470,19 +505,19 @@ export function isRGBString(value, isString = typeof value === 'string') {
470
505
  }
471
506
 
472
507
  export function isHSLAString(value, isString = typeof value === 'string') {
473
- return isString && value.includes('hsla');
508
+ return isString && value.startsWith('hsla');
474
509
  }
475
510
 
476
511
  export function isHSLString(value, isString = typeof value === 'string') {
477
- return isString && value.includes('hsl');
512
+ return isString && value.startsWith('hsl');
478
513
  }
479
514
 
480
515
  export function isVec3String(value, isString = typeof value === 'string') {
481
- return isString && value.includes('vec3(');
516
+ return isString && value.startsWith('vec3(');
482
517
  }
483
518
 
484
519
  export function isVec4String(value, isString = typeof value === 'string') {
485
- return isString && value.includes('vec4(');
520
+ return isString && value.startsWith('vec4(');
486
521
  }
487
522
 
488
523
  export function isRGBAObject(value) {
@@ -516,6 +551,16 @@ export function isRGBObject(value) {
516
551
  return false;
517
552
  }
518
553
 
554
+ export function isRGBObjectString(value) {
555
+ const parsable = value.replace(/([{,]\s*)([a-z]+)(\s*:)/g, '$1"$2"$3');
556
+ return typeof value === 'string' && isRGBObject(JSON.parse(parsable));
557
+ }
558
+
559
+ export function isRGBAObjectString(value) {
560
+ const parsable = value.replace(/([{,]\s*)([a-z]+)(\s*:)/g, '$1"$2"$3');
561
+ return typeof value === 'string' && isRGBAObject(JSON.parse(parsable));
562
+ }
563
+
519
564
  export function isVec3Array(value) {
520
565
  if (Array.isArray(value)) {
521
566
  return value.length == 3 && value.every((c) => c >= 0 && c <= 1);
@@ -575,6 +620,8 @@ export function getColorFormat(value) {
575
620
  if (isRGBString(value)) return FORMATS.RGB_STRING;
576
621
  if (isRGBObject(value)) return FORMATS.RGB_OBJECT;
577
622
  if (isRGBAObject(value)) return FORMATS.RGBA_OBJECT;
623
+ if (isRGBObjectString(value)) return FORMATS.RGB_OBJECT_STRING;
624
+ if (isRGBAObjectString(value)) return FORMATS.RGBA_OBJECT_STRING;
578
625
  if (isVec3String(value)) return FORMATS.VEC3_STRING;
579
626
  if (isVec4String(value)) return FORMATS.VEC4_STRING;
580
627
  if (isVec3Array(value)) return FORMATS.VEC3_ARRAY;
@@ -585,16 +632,26 @@ export function getColorFormat(value) {
585
632
  }
586
633
 
587
634
  export function componentsToFormat(components, format) {
588
- const [r, g, b, a] = components;
635
+ const [r, g, b, a = 1] = components;
589
636
 
590
637
  switch (format) {
638
+ case FORMATS.HEX_STRING:
639
+ return componentsToHex([r, g, b, a]);
591
640
  case FORMATS.RGB_STRING:
592
- return componentsToRGBString(components);
641
+ return componentsToRGBString([r, g, b, a]);
593
642
  case FORMATS.RGBA_STRING:
594
643
  return componentsToRGBAString([r, g, b, a]);
595
644
  case FORMATS.VEC3_STRING:
596
- return componentsToVec3String(components);
645
+ return componentsToVec3String([r, g, b, a]);
597
646
  case FORMATS.VEC4_STRING:
598
- return componentsToVec4String(components);
647
+ return componentsToVec4String([r, g, b, a]);
648
+ case FORMATS.RGB_OBJECT:
649
+ return componentsToRGBObject([r, g, b, a]);
650
+ case FORMATS.RGBA_OBJECT:
651
+ return componentsToRGBAObject([r, g, b, a]);
652
+ case FORMATS.RGB_OBJECT_STRING:
653
+ return componentsToRGBObjectString([r, g, b, a]);
654
+ case FORMATS.RGBA_OBJECT_STRING:
655
+ return componentsToRGBAObjectString([r, g, b, a]);
599
656
  }
600
657
  }
@@ -14,17 +14,48 @@ export const fieldTypes = {
14
14
  IMPORT: 'import',
15
15
  IMAGE: 'image',
16
16
  INTERVAL: 'interval',
17
+ WRAPPER: 'wrapper',
18
+ PALETTE: 'palette',
19
+ GRADIENT: 'gradient',
17
20
  };
18
21
 
19
22
  /** @type string[] */
20
23
  const types = Object.values(fieldTypes);
21
24
 
25
+ /**
26
+ * @param {string} url
27
+ * @returns {boolean}
28
+ */
22
29
  function isImageURL(url) {
23
- return url.match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
30
+ return (
31
+ url.match(/\.(jpeg|jpg|gif|png|webp)$/) !== null ||
32
+ url.startsWith('data:image')
33
+ );
24
34
  }
25
35
 
36
+ /**
37
+ * @param {any} value
38
+ * @returns {boolean}
39
+ */
26
40
  function isImage(value) {
27
- return typeof value === HTMLImageElement || isImageURL(value);
41
+ return (
42
+ typeof value === HTMLImageElement ||
43
+ (typeof value === 'string' && isImageURL(value))
44
+ );
45
+ }
46
+
47
+ /**
48
+ *
49
+ * @param {any[]} value
50
+ * @returns {boolean}
51
+ */
52
+ function isGradient(value) {
53
+ return value.every(
54
+ (v) =>
55
+ typeof v === 'object' &&
56
+ typeof v.position === 'number' &&
57
+ isColor(v.color),
58
+ );
28
59
  }
29
60
 
30
61
  export function inferFieldType({ type, value, params, key }) {
@@ -82,6 +113,10 @@ export function inferFieldType({ type, value, params, key }) {
82
113
  typeof params.max === 'number'
83
114
  ) {
84
115
  return fieldTypes.INTERVAL;
116
+ } else if (isArray && isGradient(value)) {
117
+ return fieldTypes.GRADIENT;
118
+ } else if (isArray && values.every((v) => isColor(v))) {
119
+ return fieldTypes.PALETTE;
85
120
  } else if (isColor(value)) {
86
121
  return fieldTypes.COLOR;
87
122
  } else if (typeof value === 'number') {
@@ -115,34 +150,41 @@ export function inferFieldType({ type, value, params, key }) {
115
150
  * @param {string} folder
116
151
  */
117
152
  export function parseFolder(folder) {
118
- const regex = /(?<name>[\w ]+)(?:\[(?<attributes>[^\]]+)\])?/g;
119
- const matches = [...folder.matchAll(regex)];
120
-
121
- const results = matches.map((match) => {
122
- return {
123
- name: match.groups.name,
124
- attributes: match.groups.attributes
125
- ? Object.fromEntries(
126
- match.groups.attributes
127
- .split(', ')
128
- .map((attr) =>
129
- attr
130
- .split('=')
131
- .map((v) =>
132
- v === 'false'
133
- ? false
134
- : v === 'true'
135
- ? true
136
- : v,
153
+ const segments = folder.split('.');
154
+ const regex = /(?<name>[^\[]+)(?:\[(?<attributes>[^\]]+)\])?/;
155
+
156
+ const results = segments
157
+ .map((segment) => {
158
+ const match = segment.match(regex);
159
+
160
+ if (match) {
161
+ return {
162
+ name: match.groups?.name,
163
+ attributes: match.groups?.attributes
164
+ ? Object.fromEntries(
165
+ match.groups.attributes
166
+ .split(', ')
167
+ .map((attr) =>
168
+ attr
169
+ .split('=')
170
+ .map((v) =>
171
+ v === 'false'
172
+ ? false
173
+ : v === 'true'
174
+ ? true
175
+ : v,
176
+ ),
137
177
  ),
138
- ),
139
- )
140
- : {},
141
- };
142
- });
178
+ )
179
+ : {},
180
+ };
181
+ }
182
+ })
183
+ .filter((result) => result !== undefined);
143
184
 
144
185
  let names = results.map((match) => match.name);
145
186
 
187
+ /** @type {string|undefined} */
146
188
  let rootId;
147
189
 
148
190
  results.forEach((match, index) => {