fragment-tools 0.2.11 → 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 (67) 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/cli/templates/blank/index.ts +1 -1
  11. package/src/cli/templates/default/index.js +10 -2
  12. package/src/cli/templates/default/index.ts +5 -2
  13. package/src/cli/templates/fragment-gl/index.ts +1 -1
  14. package/src/cli/templates/p5/index.ts +1 -1
  15. package/src/cli/templates/p5-webgl/index.ts +1 -1
  16. package/src/cli/templates/three-fragment/index.js +5 -3
  17. package/src/cli/templates/three-fragment/index.ts +5 -4
  18. package/src/cli/templates/three-orthographic/index.js +6 -1
  19. package/src/cli/templates/three-orthographic/index.ts +6 -2
  20. package/src/cli/templates/three-perspective/index.js +6 -1
  21. package/src/cli/templates/three-perspective/index.ts +6 -2
  22. package/src/client/app/actions/resize.js +8 -1
  23. package/src/client/app/attachments/draggable.js +93 -0
  24. package/src/client/app/client.js +90 -18
  25. package/src/client/app/components/IconFlip.svelte +46 -0
  26. package/src/client/app/hooks.js +25 -1
  27. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +95 -3
  28. package/src/client/app/lib/canvas-recorder/FrameRecorder.js +45 -3
  29. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +72 -13
  30. package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +43 -9
  31. package/src/client/app/lib/canvas-recorder/utils.js +18 -9
  32. package/src/client/app/modules/Params.svelte +1 -0
  33. package/src/client/app/renderers/2DRenderer.js +20 -16
  34. package/src/client/app/renderers/P5GLRenderer.js +13 -5
  35. package/src/client/app/renderers/P5Renderer.js +9 -1
  36. package/src/client/app/renderers/THREERenderer.js +63 -48
  37. package/src/client/app/state/Sketch.svelte.js +150 -10
  38. package/src/client/app/state/errors.svelte.js +19 -0
  39. package/src/client/app/state/exports.svelte.js +14 -1
  40. package/src/client/app/state/rendering.svelte.js +90 -13
  41. package/src/client/app/state/sketches.svelte.js +43 -7
  42. package/src/client/app/state/utils.svelte.js +49 -0
  43. package/src/client/app/ui/Field.svelte +63 -16
  44. package/src/client/app/ui/FieldSection.svelte +4 -4
  45. package/src/client/app/ui/ParamsOutput.svelte +7 -5
  46. package/src/client/app/ui/SketchRenderer.svelte +21 -0
  47. package/src/client/app/ui/fields/ButtonInput.svelte +2 -0
  48. package/src/client/app/ui/fields/CheckboxInput.svelte +13 -11
  49. package/src/client/app/ui/fields/ColorInput.svelte +16 -11
  50. package/src/client/app/ui/fields/GradientInput.svelte +607 -0
  51. package/src/client/app/ui/fields/Input.svelte +10 -6
  52. package/src/client/app/ui/fields/IntervalInput.svelte +27 -35
  53. package/src/client/app/ui/fields/NumberInput.svelte +51 -13
  54. package/src/client/app/ui/fields/PaletteInput.svelte +181 -0
  55. package/src/client/app/ui/fields/ProgressInput.svelte +44 -16
  56. package/src/client/app/ui/fields/TextareaInput.svelte +93 -0
  57. package/src/client/app/utils/canvas.utils.js +105 -28
  58. package/src/client/app/utils/color.utils.js +74 -17
  59. package/src/client/app/utils/fields.utils.js +70 -17
  60. package/src/client/app/utils/file.utils.js +86 -31
  61. package/src/client/app/utils/glsl.utils.js +11 -2
  62. package/src/client/app/utils/glslErrors.js +31 -21
  63. package/src/client/app/utils/index.js +28 -12
  64. package/src/client/main.js +7 -1
  65. package/src/types/global.d.ts +143 -0
  66. package/src/types/props.d.ts +41 -15
  67. package/tsconfig.json +1 -1
@@ -1,15 +1,17 @@
1
1
  <script>
2
- let {
3
- value = $bindable(),
4
- context,
5
- key = '',
6
- disabled = false,
7
- onchange,
8
- } = $props();
2
+ /**
3
+ * @typedef {Object} Props
4
+ * @property {boolean} value
5
+ * @property {boolean} disabled
6
+ * @property {(value: boolean) => void|undefined} onchange
7
+ */
9
8
 
10
- const handleChange = (event) => {
11
- onchange(value);
12
- };
9
+ /** @type {Props} */
10
+ let { value = $bindable(), disabled = false, onchange } = $props();
11
+
12
+ function handleChange() {
13
+ onchange?.(value);
14
+ }
13
15
  </script>
14
16
 
15
17
  <div class="checkbox" class:disabled>
@@ -18,7 +20,7 @@
18
20
  bind:checked={value}
19
21
  type="checkbox"
20
22
  onchange={handleChange}
21
- disabled={disabled ? 'disabled' : null}
23
+ {disabled}
22
24
  />
23
25
  </div>
24
26
 
@@ -16,18 +16,21 @@
16
16
  let textValue = $state();
17
17
  let alpha = $state(1);
18
18
  let hasAlpha = $derived(
19
- [
20
- color.FORMATS.RGBA_STRING,
21
- color.FORMATS.VEC4_STRING,
22
- color.FORMATS.VEC4_ARRAY,
23
- color.FORMATS.RGBA_OBJECT,
24
- color.FORMATS.HSLA_STRING,
25
- ].includes(format),
19
+ format &&
20
+ [
21
+ color.FORMATS.RGBA_STRING,
22
+ color.FORMATS.VEC4_STRING,
23
+ color.FORMATS.VEC4_ARRAY,
24
+ color.FORMATS.RGBA_OBJECT,
25
+ color.FORMATS.RGBA_OBJECT_STRING,
26
+ color.FORMATS.HSLA_STRING,
27
+ ].includes(format),
26
28
  );
27
29
 
28
30
  $effect(() => {
29
31
  if (hasAlpha) {
30
- const [r, g, b, a = 1] = color.toComponents(value);
32
+ const components = color.toComponents(value);
33
+ const a = components[3] ?? 1;
31
34
  alpha = a;
32
35
  } else {
33
36
  alpha = 1;
@@ -143,7 +146,7 @@
143
146
  <input
144
147
  class="input"
145
148
  type="color"
146
- disabled={disabled ? 'disabled' : null}
149
+ {disabled}
147
150
  value={hexValue}
148
151
  onblur={handleBlur}
149
152
  oninput={onInput}
@@ -212,7 +215,7 @@
212
215
  bottom: var(--gap);
213
216
 
214
217
  background-color: var(--currentColor);
215
- border-radius: calc(var(--fragment-input-border-radius) * 0.5);
218
+ border-radius: calc(var(--fragment-input-border-radius) - var(--gap));
216
219
  opacity: var(--opacity, 1);
217
220
  pointer-events: none;
218
221
  }
@@ -224,7 +227,9 @@
224
227
  var(--box-shadow-color, var(--fragment-accent-color));
225
228
  }
226
229
 
227
- .mirror:focus-within {
230
+ :global(body:not(.fragment-dragging))
231
+ .color-input:not(.disabled)
232
+ .mirror:focus-within {
228
233
  box-shadow: 0 0 0 2px
229
234
  var(--box-shadow-color, var(--fragment-accent-color));
230
235
  }
@@ -0,0 +1,607 @@
1
+ <script>
2
+ import { tick } from 'svelte';
3
+ import { draggable } from '@fragment/attachments/draggable';
4
+ import ButtonInput from './ButtonInput.svelte';
5
+ import ColorInput from './ColorInput.svelte';
6
+ import NumberInput from './NumberInput.svelte';
7
+ import IconFlip from '@fragment/components/IconFlip.svelte';
8
+
9
+ import { map, clamp, roundToStep } from '../../utils/math.utils';
10
+ import SelectChevrons from '../SelectChevrons.svelte';
11
+ import KeyBinding from '@fragment/components/KeyBinding.svelte';
12
+ import Keyboard from '@fragment/inputs/Keyboard';
13
+ import Layout from '../Layout.svelte';
14
+ import {
15
+ componentsToFormat,
16
+ getColorFormat,
17
+ toComponents,
18
+ toHex,
19
+ } from '@fragment/utils/color.utils';
20
+ import { SvelteMap } from 'svelte/reactivity';
21
+
22
+ let { value, disabled = false, onchange } = $props();
23
+
24
+ /** @type {HTMLCanvasElement | undefined} */
25
+ let canvas = $state();
26
+ /** @type {GradientStop | undefined} */
27
+ let activeStopIndex = $state(-1);
28
+ /** @type {boolean} */
29
+ let isOpen = $state(true);
30
+ /** @type {string} */
31
+ let gradientLabel = $derived(isOpen ? 'Add new stop' : 'Edit gradient');
32
+ /** @type {boolean} */
33
+ let dragging = $state(false);
34
+ /** @type {number} */
35
+ let draggingStopIndex = $state(-1);
36
+ let lastStopIndex = $state(0);
37
+ let focusedKey = $state(null);
38
+ let t;
39
+
40
+ /** @typedef GradientStop
41
+ * @property {number} position
42
+ * @property {string} color
43
+ */
44
+
45
+ let g = $state([
46
+ { position: 0, color: '#ff0000'},
47
+ { position: 0.49, color: '#ffff00'},
48
+ { position: 0.5, color: '#ff00ff'},
49
+ { position: 1, color: '#0000ff'},
50
+ ]);
51
+
52
+ let needsRefocus = $state(false);
53
+
54
+ let keys = new Map();
55
+
56
+ /** @type {GradientStop[]} */
57
+ let sortedStops = $derived.by(() => {
58
+ return value.map((stop, index) => {
59
+ const { position, color } = stop;
60
+
61
+ return {
62
+ position,
63
+ color,
64
+ index,
65
+ }
66
+ }).sort((a, b) => {
67
+ // Sort by position
68
+ if (a.position !== b.position) {
69
+ return a.position - b.position;
70
+ }
71
+ // maintain original order if positions are equal
72
+ return a.index - b.index;
73
+ }).map((stop, sortIndex) => {
74
+ stop.sortIndex = sortIndex;
75
+
76
+ return stop;
77
+ });
78
+ });
79
+
80
+ let ids = $derived(sortedStops.length > 0 ? [...mapIds.values()] : []);
81
+ let inputs = $state([]);
82
+
83
+ let lastSortIndex = null;
84
+
85
+ let gradient = $derived.by(() => {
86
+ let stops = sortedStops
87
+ .map(({ position, color }) => {
88
+ return `${toHex(color)} ${position * 100}%`;
89
+ })
90
+ .join(',');
91
+
92
+ return `linear-gradient(90deg, ${stops})`;
93
+ });
94
+
95
+ /** @type {DOMRect | undefined} */
96
+ let parentRect;
97
+
98
+ /**
99
+ *
100
+ * @param {MouseEvent} event
101
+ * @param {object} params
102
+ * @param {GradientStop} stop
103
+ * @param {HTMLElement} params.node
104
+ */
105
+ function onGradientDragStart(event, { rect, node }, stopIndex) {
106
+ dragging = true;
107
+ draggingStopIndex = stopIndex;
108
+ lastStopIndex = stopIndex;
109
+
110
+ const { parentElement } = node;
111
+
112
+ if (parentElement) {
113
+ parentRect = parentElement.getBoundingClientRect();
114
+ }
115
+ }
116
+
117
+ /**
118
+ *
119
+ * @param {MouseEvent} event
120
+ * @param {object} params
121
+ * @param {GradientStop} stop
122
+ */
123
+ function onGradientDrag(event, params, stopIndex) {
124
+ let position = clamp(
125
+ map(event.clientX, parentRect.left, parentRect.right, 0, 1),
126
+ 0,
127
+ 1,
128
+ );
129
+
130
+ let stop = sortedStops.find((s) => s.index === stopIndex);
131
+
132
+ if (stop) {
133
+ stop.position = position;
134
+ handleChange();
135
+ }
136
+ }
137
+
138
+ function onGradientDragEnd() {
139
+ dragging = false;
140
+ draggingStopIndex = -1;
141
+ handleChange();
142
+ }
143
+
144
+ /**
145
+ *
146
+ * @param {PointerEvent} event
147
+ */
148
+ function addStop(event) {
149
+ let position = 0;
150
+
151
+ if (event.pointerType === 'mouse') {
152
+ const rect = event.target.getBoundingClientRect();
153
+ const t = map(event.clientX, rect.left, rect.right, 0, 1);
154
+ position = t;
155
+
156
+ let [prevStop, nextStop] = getStopsAt(position);
157
+
158
+ let color = getColorAt(
159
+ clamp(map(position, prevStop.position, nextStop.position, 0, 1), 0, 1),
160
+ prevStop.color,
161
+ nextStop.color,
162
+ );
163
+
164
+ sortedStops.push({ position, color, index: sortedStops.length });
165
+ handleChange();
166
+ } else {
167
+ addStopFromLast();
168
+ }
169
+ }
170
+
171
+ function getColorAt(position, colorStart, colorEnd) {
172
+ const [r0, g0, b0] = toComponents(colorStart);
173
+ const [r1, g1, b1] = toComponents(colorEnd);
174
+
175
+
176
+ const r = map(position, 0, 1, r0, r1);
177
+ const g = map(position, 0, 1, g0, g1);
178
+ const b = map(position, 0, 1, b0, b1);
179
+
180
+ const format = getColorFormat(colorStart);
181
+ const color = componentsToFormat([r, g, b], format);
182
+
183
+ return color;
184
+ }
185
+
186
+ /**
187
+ *
188
+ * @param {number} position
189
+ * @returns {[GradientStop, GradientStop]}
190
+ */
191
+ function getStopsAt(position) {
192
+ let prevStopIndex = 0;
193
+
194
+ if (sortedStops.length > 1) {
195
+ for (let i = 1; i < sortedStops.length; i++) {
196
+ if (position > sortedStops[i].position) {
197
+ prevStopIndex = i;
198
+ }
199
+ }
200
+ }
201
+
202
+ let prevStop = sortedStops[prevStopIndex];
203
+ let nextStop = sortedStops[prevStopIndex + 1] ?? prevStop;
204
+
205
+ return [prevStop, nextStop];
206
+ }
207
+
208
+ function addStopFromLast() {
209
+ let position = 0;
210
+ let color = '#ff0000';
211
+
212
+ if (lastStopIndex >= 0) {
213
+ let lastStop = sortedStops.find((s) => s.index === lastStopIndex);
214
+ let lastPosition = lastStop.position;
215
+ let lastSortedStopIndex = sortedStops.findIndex(
216
+ (s) => s === lastStop,
217
+ );
218
+
219
+ if (sortedStops.length === 1) {
220
+ position = lastPosition < 0.5 ? 1 : 0;
221
+ }
222
+
223
+ if (sortedStops.length >= 2) {
224
+ let prevIndex =
225
+ sortedStops.length === 2
226
+ ? 0
227
+ : lastSortedStopIndex < sortedStops.length - 1
228
+ ? lastSortedStopIndex
229
+ : lastSortedStopIndex - 1;
230
+ let nextIndex = prevIndex + 1;
231
+
232
+ let prevStop = sortedStops[prevIndex];
233
+ let nextStop = sortedStops[nextIndex] ?? prevStop;
234
+ let prevPosition = prevStop.position;
235
+ let nextPosition = nextStop.position;
236
+ position = (nextPosition - prevPosition) * 0.5 + prevPosition;
237
+ color = getColorAt(
238
+ map(position, prevPosition, nextPosition, 0, 1),
239
+ prevStop.color,
240
+ nextStop.color,
241
+ );
242
+ }
243
+ }
244
+
245
+ sortedStops.push({ position, color, index: sortedStops.length });
246
+ handleChange();
247
+ }
248
+
249
+ function handleChange() {
250
+ sortedStops.forEach((s) => {
251
+ if (!value[s.index]) {
252
+ value[s.index] = {};
253
+ }
254
+
255
+ value[s.index].position = s.position;
256
+ value[s.index].color = s.color;
257
+ });
258
+
259
+ onchange($state.snapshot(value));
260
+ }
261
+
262
+ /**
263
+ *
264
+ * @param {KeyboardEvent} event
265
+ * @param {number} direction
266
+ */
267
+ function onKeyDown(event, direction) {
268
+ if (
269
+ event.target?.classList?.contains('gradient-grab') &&
270
+ activeStopIndex >= 0 &&
271
+ activeStopIndex < value.length
272
+ ) {
273
+ const diff = Keyboard.getStepFromEvent(event);
274
+ let stop = value[activeStopIndex];
275
+ let position = clamp(
276
+ stop.position + (diff * direction) / 100,
277
+ 0,
278
+ 1,
279
+ );
280
+ stop.position = position;
281
+ handleChange();
282
+ }
283
+ }
284
+
285
+ function handleClickGradient(event) {
286
+ event.preventDefault();
287
+
288
+ if (!isOpen) {
289
+ isOpen = true;
290
+ } else {
291
+ addStop(event);
292
+ }
293
+ }
294
+
295
+ function flip(event) {
296
+ event.preventDefault();
297
+
298
+ value.forEach((stop) => {
299
+ stop.position = 1 - stop.position;
300
+ });
301
+
302
+ onchange($state.snapshot(value));
303
+ }
304
+ </script>
305
+
306
+ <div class="gradient-input" class:extended={isOpen}>
307
+ <div class="input-container">
308
+ <div class="gradient-container" class:disabled>
309
+ <button
310
+ class="gradient"
311
+ class:opened={isOpen}
312
+ style="--fragment-gradient-bkg-color: {gradient}"
313
+ onclick={handleClickGradient}
314
+ >
315
+ <span class="visually-hidden">{gradientLabel}</span>
316
+ </button>
317
+ {#if isOpen}
318
+ {#each sortedStops as { position, color, index, id, key }, i (index)}
319
+ <button
320
+ class="gradient-grab"
321
+ class:dragging={dragging &&
322
+ draggingStopIndex === index}
323
+ style="--x: {position}; --fragment-gradient-grab-bkg-color: {toHex(color)}"
324
+ onfocus={() => {
325
+ activeStopIndex = index;
326
+ lastStopIndex = index;
327
+ }}
328
+ onblur={() => {
329
+ activeStopIndex = -1;
330
+ }}
331
+ {@attach draggable({
332
+ onDragStart: (event, params) => {
333
+ onGradientDragStart(event, params, index);
334
+ },
335
+ onDrag: (event, params) => {
336
+ onGradientDrag(event, params, index);
337
+ },
338
+ onDragEnd: () => {
339
+ onGradientDragEnd();
340
+ },
341
+ })}
342
+ >
343
+ <span class="visually-hidden">Drag</span>
344
+ </button>
345
+ {/each}
346
+ {/if}
347
+ </div>
348
+ <div class="gradient-edit">
349
+ <ButtonInput
350
+ label="open"
351
+ showLabel={false}
352
+ onclick={(event) => {
353
+ event.preventDefault();
354
+ isOpen = !isOpen;
355
+ }}
356
+ >
357
+ <SelectChevrons width={20} />
358
+ </ButtonInput>
359
+ </div>
360
+ </div>
361
+ {#if isOpen}
362
+ <div class="subgrid">
363
+ <div class="gradient-actions">
364
+ <ButtonInput label="flip" onclick={flip} showLabel={false}>
365
+ <IconFlip angle="90deg"/>
366
+ </ButtonInput>
367
+ <div class="gradient-stop-add">
368
+ <ButtonInput label="+" onclick={addStopFromLast} />
369
+ </div>
370
+ </div>
371
+ {#each sortedStops as stop, i (stop.sortIndex)}
372
+ {@const { position, color, index, id, sortIndex } = stop}
373
+ <div class="gradient-stop">
374
+ <NumberInput
375
+ bind:node={inputs[sortIndex]}
376
+ value={position * 100}
377
+ suffix="%"
378
+ step={1}
379
+ shouldFocus={focusedKey === stop.key}
380
+ onchange={(v) => {
381
+ const position = clamp(v / 100, 0, 1);
382
+ const prevPosition = stop.position;
383
+
384
+ const currentSortIndex = sortIndex;
385
+ const clone = sortedStops.map((s) => {
386
+ return {
387
+ index: s.index,
388
+ position: s.index === index ? position : s.position,
389
+ }
390
+ });
391
+
392
+ clone.sort((a, b) => {
393
+ if (a.position !== b.position) {
394
+ return a.position - b.position;
395
+ }
396
+ return a.index - b.index;
397
+ })
398
+
399
+ const newSorted = clone.map((stop, sortIndex) => {
400
+ stop.sortIndex = sortIndex;
401
+
402
+ return stop;
403
+ });
404
+
405
+ const newSortIndex = newSorted.find((s) => s.index === index).sortIndex;
406
+ const orderIsChanging = currentSortIndex !== newSortIndex && prevPosition !== position;
407
+
408
+ stop.position = position;
409
+
410
+ if (orderIsChanging) {
411
+ tick().then(() => {
412
+ inputs[newSortIndex].focus();
413
+ });
414
+ }
415
+
416
+ handleChange();
417
+
418
+ lastStopIndex = index;
419
+ }}
420
+ />
421
+ <ColorInput
422
+ value={color}
423
+ onchange={(c) => {
424
+ stop.color = c;
425
+ lastStopIndex = index;
426
+ handleChange();
427
+ }}
428
+ />
429
+ <div class="gradient-stop-delete">
430
+ <ButtonInput
431
+ label="-"
432
+ disabled={value.length === 1}
433
+ onclick={() => {
434
+ if (lastStopIndex === value.length - 1) {
435
+ lastStopIndex -= 1;
436
+ }
437
+
438
+ const baseIndex = stop.index;
439
+ value.splice(baseIndex, 1);
440
+
441
+ const index = sortedStops.indexOf(stop);
442
+
443
+ sortedStops.splice(index, 1);
444
+ handleChange();
445
+ }}
446
+ />
447
+ </div>
448
+ </div>
449
+ {/each}
450
+ </div>
451
+ {/if}
452
+ </div>
453
+
454
+ <KeyBinding
455
+ key="ArrowLeft"
456
+ type="down"
457
+ onTrigger={(event) => onKeyDown(event, -1)}
458
+ />
459
+ <KeyBinding
460
+ key="ArrowUp"
461
+ type="down"
462
+ onTrigger={(event) => onKeyDown(event, 1)}
463
+ />
464
+ <KeyBinding
465
+ key="ArrowDown"
466
+ type="down"
467
+ onTrigger={(event) => onKeyDown(event, -1)}
468
+ />
469
+ <KeyBinding
470
+ key="ArrowRight"
471
+ type="down"
472
+ onTrigger={(event) => onKeyDown(event, 1)}
473
+ />
474
+
475
+ <style>
476
+ .gradient-input {
477
+ --grab-height: 16px;
478
+
479
+ position: relative;
480
+ width: 100%;
481
+
482
+ display: flex;
483
+ flex-direction: column;
484
+ row-gap: var(--column-gap);
485
+ }
486
+
487
+ .input-container {
488
+ display: grid;
489
+ grid-template-columns: 1fr auto;
490
+ gap: var(--column-gap);
491
+ align-items: start;
492
+ margin: 2px 0;
493
+ }
494
+
495
+ .gradient-container {
496
+ position: relative;
497
+
498
+ display: flex;
499
+
500
+ gap: var(--column-gap);
501
+
502
+ border-radius: var(--fragment-input-border-radius);
503
+ container-type: inline-size;
504
+ }
505
+
506
+ .gradient {
507
+ position: relative;
508
+ width: 100%;
509
+ height: var(--fragment-input-height);
510
+
511
+ cursor: pointer;
512
+ background: var(--fragment-gradient-bkg-color);
513
+ border-radius: calc(var(--fragment-input-border-radius));
514
+ box-shadow: inset 0 0 0 1px var(--fragment-input-border-color);
515
+ outline: 0;
516
+ }
517
+
518
+ .gradient:focus-visible {
519
+ box-shadow: inset 0 0 0 2px var(--fragment-accent-color);
520
+ }
521
+
522
+ .gradient-input.extended {
523
+ .gradient {
524
+ cursor: cell;
525
+ }
526
+ }
527
+
528
+ .gradient-input:not(.extended) .gradient-edit :global(svg) {
529
+ transform: rotate(-90deg);
530
+ }
531
+
532
+ .gradient-grab {
533
+ --size: 8px;
534
+ --flow: 1px;
535
+ --container-width: calc(100 * 1cqw - 1px);
536
+ position: absolute;
537
+ top: calc(var(--fragment-input-height) + var(--column-gap) - 1px);
538
+ left: calc(1px - var(--size) * 0.5);
539
+ /*left: -1px;*/
540
+
541
+ width: var(--size);
542
+ height: var(--grab-height);
543
+
544
+ background-color: var(--fragment-gradient-grab-bkg-color);
545
+
546
+ transform: translateX(calc(var(--x) * (var(--container-width))));
547
+ border-radius: var(--fragment-input-border-radius);
548
+
549
+ cursor: grab;
550
+ box-shadow: inset 0 0 0 1px var(--fragment-input-border-color);
551
+ outline: 0;
552
+ /*border: 2px solid var(--fragment-accent-color);*/
553
+
554
+ /*&:before {
555
+ content: '';
556
+ position: absolute;
557
+ inset: 0;
558
+
559
+ border: 1px solid var(--fragment-input-border-color);
560
+ }*/
561
+ }
562
+
563
+ :global(body:not(.fragment-dragging)) .gradient-grab:not(.disabled):hover {
564
+ box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
565
+ }
566
+
567
+ .gradient-grab.dragging,
568
+ :global(body:not(.fragment-dragging))
569
+ .gradient-grab:not(.disabled):focus-visible {
570
+ box-shadow: 0 0 0 2px var(--fragment-accent-color);
571
+ }
572
+
573
+ .gradient-input.extended .gradient-actions {
574
+ margin-top: calc(var(--grab-height));
575
+ }
576
+
577
+ .subgrid {
578
+ display: grid;
579
+ grid-template-columns: 1fr 4fr auto;
580
+ column-gap: var(--column-gap);
581
+ }
582
+
583
+ .gradient-actions {
584
+ display: grid;
585
+ grid-template-columns: subgrid;
586
+ grid-column: 1 / -1;
587
+ padding-bottom: 3px;
588
+ }
589
+
590
+ .gradient-stop-add {
591
+ grid-column: 2 / -1;
592
+ }
593
+
594
+ .gradient-stop {
595
+ display: grid;
596
+ grid-template-columns: subgrid;
597
+ grid-column: 1 / -1;
598
+ }
599
+
600
+ .gradient-stop-delete {
601
+ margin-top: 2px;
602
+ }
603
+
604
+ :global(body:not(.fragment-dragging)) .gradient:not(.disabled):hover {
605
+ box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
606
+ }
607
+ </style>