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,3 +1,4 @@
1
+ import { saveFiles } from '@fragment/utils/file.utils';
1
2
  import { screenshotCanvas, recordCanvas } from '../utils/canvas.utils';
2
3
  import { hydrate, persist } from './utils.svelte';
3
4
 
@@ -58,6 +59,7 @@ class Exports {
58
59
  imageCount = $state(1);
59
60
  recording = $state(false);
60
61
  capturing = $state(false);
62
+ committing = $state(false);
61
63
  imageCollapsed = $state(false);
62
64
  videoCollapsed = $state(false);
63
65
 
@@ -96,6 +98,8 @@ class Exports {
96
98
  quality = this.imageQuality,
97
99
  pixelsPerInch = this.pixelsPerInch,
98
100
  filename,
101
+ files = [],
102
+ commit = false,
99
103
  pattern,
100
104
  exportDir,
101
105
  params = {},
@@ -117,7 +121,7 @@ class Exports {
117
121
  for (let i = 0; i < count; i++) {
118
122
  onBeforeCapture(captureParams);
119
123
 
120
- await screenshotCanvas(canvas, {
124
+ const file = screenshotCanvas(canvas, {
121
125
  filename,
122
126
  pattern,
123
127
  exportDir,
@@ -128,10 +132,19 @@ class Exports {
128
132
  pixelsPerInch,
129
133
  });
130
134
 
135
+ files.push(file);
136
+
131
137
  onAfterCapture(captureParams);
132
138
  }
133
139
 
134
140
  onComplete(captureParams);
141
+
142
+ try {
143
+ await saveFiles(files, [], { commit });
144
+ } catch (error) {
145
+ console.error(`[fragment] Error while saving screenshot.`);
146
+ console.log(error);
147
+ }
135
148
  }
136
149
 
137
150
  record(
@@ -12,6 +12,11 @@ import { layout } from './layout.svelte.js';
12
12
  import { persist, hydrate } from './utils.svelte';
13
13
  import presets from '../lib/presets';
14
14
  import { client } from '../client.js';
15
+ import { saveFiles } from '@fragment/utils/file.utils.js';
16
+ import {
17
+ defaultFilenamePattern,
18
+ getFilenameParams,
19
+ } from '@fragment/utils/canvas.utils.js';
15
20
 
16
21
  export const SIZES = {
17
22
  FIXED: 'fixed',
@@ -94,6 +99,10 @@ class Rendering {
94
99
  this.estimateRefreshRate();
95
100
  }
96
101
 
102
+ /**
103
+ *
104
+ * @param {string} renderingMode
105
+ */
97
106
  loadRenderer(renderingMode) {
98
107
  if (__THREE_RENDERER__ && renderingMode === 'three') {
99
108
  return import('../renderers/THREERenderer.js');
@@ -321,7 +330,8 @@ export class Render {
321
330
  this.time = 0;
322
331
  this.recording = false;
323
332
 
324
- let resizeTimeout;
333
+ /** @type {number|null} */
334
+ let resizeTimeout = null;
325
335
 
326
336
  $effect.pre(() => {
327
337
  const { width, height, pixelRatio } = rendering;
@@ -330,7 +340,10 @@ export class Render {
330
340
  if (resizeTimeout) clearTimeout(resizeTimeout);
331
341
 
332
342
  resizeTimeout = setTimeout(() => {
333
- clearTimeout(resizeTimeout);
343
+ if (resizeTimeout) {
344
+ clearTimeout(resizeTimeout);
345
+ }
346
+
334
347
  resizeTimeout = null;
335
348
 
336
349
  this.resize(width, height, pixelRatio);
@@ -399,6 +412,7 @@ export class Render {
399
412
  this.then = performance.now();
400
413
 
401
414
  this.playhead = 0;
415
+ this.playheadLast = 0;
402
416
  this.playcount = 0;
403
417
  this.frame = 0;
404
418
 
@@ -443,24 +457,23 @@ export class Render {
443
457
  return;
444
458
  }
445
459
 
446
- let playhead = time / 1000 / duration;
447
- playhead %= 1;
448
- let playcount = 0;
460
+ let totalPlayhead = time / 1000 / duration;
461
+ let playhead = fps === 0 ? 0 : totalPlayhead % 1;
462
+
463
+ if (playhead < this.playheadLast) {
464
+ this.playcount++;
465
+ }
466
+ this.playheadLast = playhead;
467
+
449
468
  if (isFinite(interval)) {
450
469
  // round values for low framerates
451
470
  playhead = Math.floor(playhead / interval) * interval;
452
471
  }
453
- if (isFinite(duration)) {
454
- playcount = Math.floor(this.timeTotal / 1000 / duration);
455
- }
456
- if (fps === 0) {
457
- playhead = 0;
458
- }
472
+
459
473
  const now = performance.now();
460
474
  const deltaTime = now - this.thenLoop;
461
475
 
462
476
  this.playhead = playhead;
463
- this.playcount = playcount;
464
477
  this.frame = Math.floor(map(playhead, 0, 1, 1, frameCount + 1));
465
478
  if (
466
479
  this.elapsed === 0 ||
@@ -499,6 +512,9 @@ export class Render {
499
512
 
500
513
  this.raf = requestAnimationFrame(() => {
501
514
  this.time = new Date().getTime() - rendering.today;
515
+ this.initialTime = this.time;
516
+ this.playheadLast = 0;
517
+ this.playcount = 0;
502
518
  this.then = this.time;
503
519
  this.thenLoop = performance.now();
504
520
  this.update(this.time);
@@ -522,6 +538,14 @@ export class Render {
522
538
  });
523
539
  }
524
540
 
541
+ /**
542
+ *
543
+ * @param {Object} params
544
+ * @param {HTMLElement} params.container
545
+ * @param {HTMLCanvasElement} [params.canvas]
546
+ * @param {string} params.context
547
+ * @returns
548
+ */
525
549
  createCanvas({
526
550
  container,
527
551
  canvas = document.createElement('canvas'),
@@ -543,6 +567,10 @@ export class Render {
543
567
  return canvas;
544
568
  }
545
569
 
570
+ /**
571
+ *
572
+ * @param {HTMLCanvasElement} canvas
573
+ */
546
574
  destroyCanvas(canvas) {
547
575
  if (canvas) {
548
576
  this.observer.disconnect();
@@ -551,7 +579,6 @@ export class Render {
551
579
  canvas.onmousemove = null;
552
580
  canvas.onmouseup = null;
553
581
  canvas.onclick = null;
554
- canvas = null;
555
582
  }
556
583
  }
557
584
 
@@ -559,6 +586,8 @@ export class Render {
559
586
  filename = this.sketch.key,
560
587
  pattern = this.sketch.filenamePattern,
561
588
  exportDir = this.sketch.exportDir,
589
+ files = [],
590
+ commit = false,
562
591
  } = {}) {
563
592
  const { sketch } = this;
564
593
 
@@ -569,6 +598,8 @@ export class Render {
569
598
  params: {
570
599
  props: sketch.props,
571
600
  },
601
+ files,
602
+ commit,
572
603
  onBeforeCapture: (params) => {
573
604
  sketch.beforeCapture.forEach((fn) => fn(params));
574
605
  this.renderSketch();
@@ -580,6 +611,44 @@ export class Render {
580
611
  });
581
612
  }
582
613
 
614
+ async commit({
615
+ filename = this.sketch.key,
616
+ pattern = this.sketch.filenamePattern ?? defaultFilenamePattern,
617
+ exportDir = this.sketch.exportDir,
618
+ } = {}) {
619
+ const { sketch } = this;
620
+ const { props } = sketch;
621
+
622
+ const data = {};
623
+
624
+ for (const key in props) {
625
+ data[key] = { value: props[key].value };
626
+ }
627
+
628
+ let patternParams = getFilenameParams();
629
+ let name = pattern({
630
+ filename,
631
+ params: { props },
632
+ ...patternParams,
633
+ });
634
+
635
+ let files = [
636
+ {
637
+ filename: `${name}.props.json`,
638
+ exportDir,
639
+ data: JSON.stringify(data),
640
+ },
641
+ ];
642
+
643
+ await this.screenshot({
644
+ filename,
645
+ pattern,
646
+ exportDir,
647
+ files,
648
+ commit: true,
649
+ });
650
+ }
651
+
583
652
  get params() {
584
653
  return {
585
654
  ...this.mountParams,
@@ -616,6 +685,8 @@ export class Render {
616
685
  onStart: (params) => {
617
686
  this.time = 0;
618
687
  this.elapsed = 0;
688
+ this.playcount = 0;
689
+ this.playheadLast = 0;
619
690
  this.thenLoop = performance.now();
620
691
 
621
692
  sketch.beforeRecord.forEach((fn) => fn(params));
@@ -686,6 +757,10 @@ export class Render {
686
757
  }
687
758
  }
688
759
 
760
+ /**
761
+ *
762
+ * @param {number} time
763
+ */
689
764
  update(time) {
690
765
  if (!this.paused) {
691
766
  const deltaTime = time - this.then;
@@ -705,6 +780,8 @@ export class Render {
705
780
  invalidate() {
706
781
  this.time = 0;
707
782
  this.elapsed = 0;
783
+ this.playcount = 0;
784
+ this.playheadLast = 0;
708
785
  }
709
786
 
710
787
  dispose() {
@@ -6,11 +6,28 @@ import Sketch from './Sketch.svelte.js';
6
6
  import { rendering } from './rendering.svelte.js';
7
7
  import { removeHotListeners } from '../triggers/index.js';
8
8
 
9
+ /**
10
+ * @typedef {Object} SketchInstance
11
+ * @property {string} [rendering]
12
+ * @property {any} [renderer]
13
+ */
14
+
15
+ /**
16
+ * @typedef {Record<string, () => Promise<SketchInstance>>} SketchCollection
17
+ */
18
+
9
19
  class SketchesManager {
20
+ /** @type {Record<string, Sketch>} */
10
21
  sketches = $state({});
11
22
  keys = $derived(Object.keys(this.sketches));
12
23
  count = $derived(this.keys.length);
13
24
 
25
+ /**
26
+ * Load a single sketch from a collection
27
+ * @param {SketchCollection} collection - The collection of sketches
28
+ * @param {string} key - The key of the sketch to load
29
+ * @returns {Promise<SketchInstance | undefined>}
30
+ */
14
31
  async loadSketch(collection, key) {
15
32
  try {
16
33
  let sketch = await collection[key]();
@@ -27,6 +44,11 @@ class SketchesManager {
27
44
  }
28
45
  }
29
46
 
47
+ /**
48
+ * Load all sketches from a collection
49
+ * @param {SketchCollection} collection - The collection of sketches to load
50
+ * @returns {Promise<void>}
51
+ */
30
52
  async loadAll(collection) {
31
53
  const keys = [...Object.keys(collection)];
32
54
 
@@ -42,16 +64,30 @@ class SketchesManager {
42
64
  keys.map((key) => this.loadSketch(collection, key)),
43
65
  );
44
66
 
45
- const newSketches = keys.reduce((all, key, index) => {
46
- if (loadedSketches[index]) {
47
- all[key] = loadedSketches[index];
48
- }
67
+ /** @type {Record<string, SketchInstance>} */
68
+ const newSketches = keys.reduce(
69
+ /**
70
+ * @param {Record<string, SketchInstance>} all
71
+ * @param {string} key
72
+ * @param {number} index
73
+ */
74
+ (all, key, index) => {
75
+ if (loadedSketches[index]) {
76
+ all[key] = loadedSketches[index];
77
+ }
49
78
 
50
- return all;
51
- }, {});
79
+ return all;
80
+ },
81
+ {},
82
+ );
52
83
 
84
+ /** @type {Record<string, Sketch>} */
53
85
  const newInstancedSketches = Object.keys(newSketches).reduce(
54
- (all, key, index) => {
86
+ /**
87
+ * @param {Record<string, Sketch>} all
88
+ * @param {string} key
89
+ */
90
+ (all, key) => {
55
91
  const prevSketch = this.sketches[key];
56
92
 
57
93
  const instanced = new Sketch({
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Persists data to localStorage with a "fragment." prefix
3
+ * @param {string} key - The storage key (will be prefixed with "fragment.")
4
+ * @param {any} data - The data to store (will be JSON stringified)
5
+ * @throws {Error} If localStorage operation fails
6
+ */
1
7
  export function persist(key, data) {
2
8
  try {
3
9
  window.localStorage.setItem(`fragment.${key}`, JSON.stringify(data));
@@ -6,6 +12,13 @@ export function persist(key, data) {
6
12
  }
7
13
  }
8
14
 
15
+ /**
16
+ * Retrieves and optionally merges data from localStorage
17
+ * @param {string} key - The storage key (will be prefixed with "fragment.")
18
+ * @param {Record<string, any>} [target={}] - Optional target object to merge data into
19
+ * @param {any} [defaultValue={}] - Default value to return if no data found
20
+ * @returns {any} The retrieved data, defaultValue if not found, or undefined on error
21
+ */
9
22
  export function hydrate(key, target = {}, defaultValue = {}) {
10
23
  try {
11
24
  const storageKey = `fragment.${key}`;
@@ -31,14 +44,39 @@ export function hydrate(key, target = {}, defaultValue = {}) {
31
44
  }
32
45
  }
33
46
 
47
+ /**
48
+ * Checks if a value is an object
49
+ * @param {any} item - The value to check
50
+ * @returns {boolean} True if the value is an object
51
+ */
34
52
  export function isObject(item) {
35
53
  return item && typeof item === 'object';
36
54
  }
37
55
 
56
+ /**
57
+ * Checks if a value is a function
58
+ * @param {any} item - The value to check
59
+ * @returns {boolean} True if the value is a function
60
+ */
38
61
  export function isFunction(item) {
39
62
  return item && typeof item === 'function';
40
63
  }
41
64
 
65
+ /**
66
+ * Returns true if the given cache key contains the data:image scheme.
67
+ * @param {any} value
68
+ * @return {boolean} Whether the given cache url contains the blob: scheme or not.
69
+ */
70
+ export function isDataURL(value) {
71
+ return typeof value === 'string' && value.startsWith('data:image/');
72
+ }
73
+
74
+ /**
75
+ * Recursively assigns properties from source to target
76
+ * @param {Record<string, any>} target - The target object to assign to
77
+ * @param {Record<string, any>} source - The source object to assign from
78
+ * @returns {void}
79
+ */
42
80
  export function deepAssign(target, source) {
43
81
  for (const key in source) {
44
82
  if (isObject(source[key]) && isObject(target[key])) {
@@ -49,6 +87,12 @@ export function deepAssign(target, source) {
49
87
  }
50
88
  }
51
89
 
90
+ /**
91
+ * Recursively compares two values for deep equality
92
+ * @param {any} target - The first value to compare
93
+ * @param {any} source - The second value to compare
94
+ * @returns {boolean} True if the values are deeply equal
95
+ */
52
96
  export function deepEqual(target, source) {
53
97
  if (isObject(target) && isObject(source)) {
54
98
  let isEqual = true;
@@ -75,6 +119,11 @@ export function deepEqual(target, source) {
75
119
  return target === source;
76
120
  }
77
121
 
122
+ /**
123
+ * Creates a deep clone of a value
124
+ * @param {any} value - The value to clone
125
+ * @returns {any} A deep clone of the value, or the original value if cloning fails
126
+ */
78
127
  export function deepClone(value) {
79
128
  if (isFunction(value)) {
80
129
  return value;
@@ -4,11 +4,14 @@
4
4
  import CheckboxInput from './fields/CheckboxInput.svelte';
5
5
  import VectorInput from './fields/VectorInput.svelte';
6
6
  import TextInput from './fields/TextInput.svelte';
7
+ import TextareaInput from './fields/TextareaInput.svelte';
7
8
  import ColorInput from './fields/ColorInput.svelte';
8
9
  import ListInput from './fields/ListInput.svelte';
9
10
  import ButtonInput from './fields/ButtonInput.svelte';
10
11
  import ImageInput from './fields/ImageInput.svelte';
11
12
  import IntervalInput from './fields/IntervalInput.svelte';
13
+ import PaletteInput from './fields/PaletteInput.svelte';
14
+ import GradientInput from './fields/GradientInput.svelte';
12
15
  import { fieldTypes } from '../utils/fields.utils.js';
13
16
 
14
17
  const fields = {
@@ -17,13 +20,17 @@
17
20
  [`${fieldTypes.VEC}`]: VectorInput,
18
21
  [`${fieldTypes.CHECKBOX}`]: CheckboxInput,
19
22
  [`${fieldTypes.TEXT}`]: TextInput,
23
+ [`${fieldTypes.TEXTAREA}`]: TextareaInput,
20
24
  [`${fieldTypes.LIST}`]: ListInput,
21
25
  [`${fieldTypes.COLOR}`]: ColorInput,
26
+ [`${fieldTypes.PALETTE}`]: PaletteInput,
22
27
  [`${fieldTypes.BUTTON}`]: ButtonInput,
23
28
  [`${fieldTypes.DOWNLOAD}`]: ButtonInput,
24
29
  [`${fieldTypes.IMPORT}`]: ImportInput,
25
30
  [`${fieldTypes.IMAGE}`]: ImageInput,
26
31
  [`${fieldTypes.INTERVAL}`]: IntervalInput,
32
+ [`${fieldTypes.GRADIENT}`]: GradientInput,
33
+ [`${fieldTypes.WRAPPER}`]: null,
27
34
  };
28
35
  </script>
29
36
 
@@ -52,6 +59,7 @@
52
59
  onchange,
53
60
  onclick = () => {},
54
61
  children,
62
+ trackChanges = false,
55
63
  triggers = $bindable([]),
56
64
  } = $props();
57
65
 
@@ -63,10 +71,18 @@
63
71
 
64
72
  onchange(value);
65
73
  },
74
+ /**
75
+ *
76
+ * @param {MouseEvent} event
77
+ */
66
78
  button: (event) => {
67
79
  value(event);
68
80
  onclick(event);
69
81
  },
82
+ /**
83
+ *
84
+ * @param {MouseEvent} event
85
+ */
70
86
  download: async (event) => {
71
87
  try {
72
88
  let [data, filename] = await value(event);
@@ -105,7 +121,12 @@
105
121
  fieldType === fieldTypes.BUTTON),
106
122
  );
107
123
  let triggersActive = $derived(triggers.length > 0);
124
+ let changed = $derived(trackChanges && !deepEqual(value, initialValue));
108
125
 
126
+ /**
127
+ *
128
+ * @param {MouseEvent} event
129
+ */
109
130
  function toggleTriggers(event) {
110
131
  event.preventDefault();
111
132
 
@@ -123,16 +144,15 @@
123
144
  };
124
145
  }
125
146
 
126
- function hasChanged(current, next) {
127
- const changed = !deepEqual(current, next);
128
- return changed;
147
+ function restoreInitialValue() {
148
+ onchange($state.snapshot(initialValue));
129
149
  }
130
150
  </script>
131
151
 
132
152
  <div
133
153
  class="field"
134
154
  class:disabled
135
- class:changed={!disabled && hasChanged(value, initialValue)}
155
+ class:changed={!disabled && changed}
136
156
  style="--index: {index};"
137
157
  >
138
158
  <FieldSection
@@ -166,6 +186,15 @@
166
186
  <Component {value} {...fieldProps} {onchange} onclick={onTrigger} />
167
187
  {@render children?.()}
168
188
  </FieldSection>
189
+ {#if changed}
190
+ <button
191
+ class="field__changed"
192
+ onclick={restoreInitialValue}
193
+ title="Restore initial value"
194
+ >
195
+ <span class="visually-hidden">Restore initial value</span>
196
+ </button>
197
+ {/if}
169
198
  {#if triggerable}
170
199
  <FieldSection {key} visible={showTriggers} secondary>
171
200
  <FieldTriggers
@@ -192,28 +221,45 @@
192
221
  border-bottom: 1px solid var(--fragment-spacing-color);
193
222
  }
194
223
 
195
- .field.changed:before {
196
- content: '';
197
-
224
+ .field__changed {
198
225
  position: absolute;
199
226
  top: 0px;
200
227
  left: 0px;
201
228
  bottom: 0px;
202
229
  z-index: 1;
203
230
 
204
- width: 4px;
231
+ width: 13px;
205
232
  /* height: 4px; */
206
233
  /* border-radius: 2px; */
207
234
 
208
- --stripes-offset: calc(var(--index) * 1.9px);
235
+ background: transparent;
236
+ cursor: pointer;
237
+
238
+ &:before {
239
+ content: '';
240
+
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+
245
+ display: block;
246
+ width: 4px;
247
+ height: 100%;
248
+
249
+ --stripes-offset: calc(var(--index) * 1.9px);
250
+
251
+ background: repeating-linear-gradient(
252
+ 45deg,
253
+ var(--fragment-accent-color) calc(0px + var(--stripes-offset)),
254
+ var(--fragment-accent-color) calc(2px + var(--stripes-offset)),
255
+ transparent calc(2px + var(--stripes-offset)),
256
+ transparent calc(4px + var(--stripes-offset))
257
+ );
258
+ }
209
259
 
210
- background: repeating-linear-gradient(
211
- 45deg,
212
- var(--fragment-accent-color) calc(0px + var(--stripes-offset)),
213
- var(--fragment-accent-color) calc(2px + var(--stripes-offset)),
214
- transparent calc(2px + var(--stripes-offset)),
215
- transparent calc(4px + var(--stripes-offset))
216
- );
260
+ &:hover:before {
261
+ width: 7px;
262
+ }
217
263
  }
218
264
 
219
265
  :global(.field__input .field) {
@@ -229,6 +275,7 @@
229
275
  .field__actions {
230
276
  display: flex;
231
277
  align-items: center;
278
+ gap: var(--column-gap);
232
279
  }
233
280
 
234
281
  .field__action {
@@ -2,13 +2,13 @@
2
2
  let {
3
3
  key,
4
4
  visible = true,
5
- secondary,
6
- interactive,
5
+ secondary = false,
6
+ interactive = false,
7
7
  displayName = undefined,
8
8
  disabled = false,
9
9
  children,
10
- infos,
11
- onclick,
10
+ infos = undefined,
11
+ onclick = () => {},
12
12
  } = $props();
13
13
  </script>
14
14
 
@@ -101,8 +101,8 @@
101
101
  }}
102
102
  />
103
103
  {/if}
104
- <!-- {#if rendering.resizing === SIZES.PRESET}
105
- <Field key="preset">
104
+ {#if rendering.resizing === SIZES.PRESET}
105
+ <Field key="preset" type="wrapper" value={`${rendering.preset}-${rendering.presetOrientation}`}>
106
106
  <FieldInputRow --grid-template-columns="1fr 1fr">
107
107
  <Select
108
108
  value={rendering.preset}
@@ -123,18 +123,20 @@
123
123
  />
124
124
  </FieldInputRow>
125
125
  </Field>
126
- {/if} -->
127
-
126
+ {/if}
128
127
  {#if rendering.resizing !== SIZES.PRESET}
129
128
  <Field
130
129
  key="pixelRatio"
131
130
  value={Number(rendering.pixelRatio)}
132
- onchange={(pixelRatio) => (rendering.pixelRatio = pixelRatio)}
131
+ onchange={(pixelRatio) => {
132
+ rendering.pixelRatio = pixelRatio;
133
+ }}
133
134
  params={{
134
135
  step: 0.1,
135
136
  }}
136
137
  />
137
138
  {/if}
139
+
138
140
  <!-- {#if $sketchesCount > 1 && $monitors.length > 1}
139
141
  <ParamsMultisampling />
140
142
  {/if} -->
@@ -69,6 +69,16 @@
69
69
  } else if (render?.recording && !exports.recording) {
70
70
  render.stopRecording();
71
71
  }
72
+
73
+ if (exports.capturing) {
74
+ render.screenshot();
75
+ exports.capturing = false;
76
+ }
77
+
78
+ if (exports.committing) {
79
+ render.commit();
80
+ exports.committing = false;
81
+ }
72
82
  });
73
83
 
74
84
  function checkForRefresh(event) {
@@ -108,6 +118,16 @@
108
118
  }
109
119
  }
110
120
 
121
+ function checkForCommit(event) {
122
+ if (event.metaKey || event.ctrlKey) {
123
+ event.preventDefault();
124
+
125
+ if (!exports.committing) {
126
+ render.commit();
127
+ }
128
+ }
129
+ }
130
+
111
131
  let backgroundColor = $derived.by(() => {
112
132
  if (layout.previewing) {
113
133
  return (
@@ -149,6 +169,7 @@
149
169
  <KeyBinding type="down" key=" " onTrigger={checkForPause} />
150
170
  <KeyBinding type="down" key="s" onTrigger={checkForScreenshot} />
151
171
  <KeyBinding type="down" key="S" onTrigger={checkForRecord} />
172
+ <KeyBinding type="down" key="k" onTrigger={checkForCommit} />
152
173
 
153
174
  <style>
154
175
  .sketch-renderer {
@@ -32,6 +32,8 @@
32
32
  }
33
33
 
34
34
  .button {
35
+ position: relative;
36
+
35
37
  display: flex;
36
38
  width: 100%;
37
39
  min-width: var(--fragment-input-height);