fragment-tools 0.2.12 → 0.2.14

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 (55) 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 +8 -3
  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/FragmentRenderer.js +1 -1
  22. package/src/client/app/renderers/P5GLRenderer.js +14 -6
  23. package/src/client/app/renderers/P5Renderer.js +9 -1
  24. package/src/client/app/renderers/THREERenderer.js +62 -48
  25. package/src/client/app/state/Sketch.svelte.js +149 -9
  26. package/src/client/app/state/errors.svelte.js +19 -0
  27. package/src/client/app/state/exports.svelte.js +14 -1
  28. package/src/client/app/state/rendering.svelte.js +47 -0
  29. package/src/client/app/state/sketches.svelte.js +43 -7
  30. package/src/client/app/state/utils.svelte.js +49 -0
  31. package/src/client/app/ui/Field.svelte +6 -1
  32. package/src/client/app/ui/FieldSection.svelte +4 -4
  33. package/src/client/app/ui/ParamsOutput.svelte +1 -1
  34. package/src/client/app/ui/SketchRenderer.svelte +16 -0
  35. package/src/client/app/ui/fields/ButtonInput.svelte +2 -0
  36. package/src/client/app/ui/fields/CheckboxInput.svelte +13 -11
  37. package/src/client/app/ui/fields/ColorInput.svelte +16 -11
  38. package/src/client/app/ui/fields/GradientInput.svelte +607 -0
  39. package/src/client/app/ui/fields/Input.svelte +10 -6
  40. package/src/client/app/ui/fields/IntervalInput.svelte +27 -35
  41. package/src/client/app/ui/fields/NumberInput.svelte +51 -13
  42. package/src/client/app/ui/fields/PaletteInput.svelte +181 -0
  43. package/src/client/app/ui/fields/ProgressInput.svelte +44 -16
  44. package/src/client/app/ui/fields/TextareaInput.svelte +10 -10
  45. package/src/client/app/utils/canvas.utils.js +105 -28
  46. package/src/client/app/utils/color.utils.js +74 -17
  47. package/src/client/app/utils/fields.utils.js +68 -26
  48. package/src/client/app/utils/file.utils.js +86 -31
  49. package/src/client/app/utils/glsl.utils.js +11 -2
  50. package/src/client/app/utils/glslErrors.js +31 -21
  51. package/src/client/app/utils/index.js +28 -12
  52. package/src/client/main.js +7 -1
  53. package/src/types/global.d.ts +143 -0
  54. package/src/types/props.d.ts +40 -15
  55. package/tsconfig.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { styleText } from 'node:util';
1
2
  import { ConfirmPrompt, SelectPrompt, TextPrompt } from '@clack/core';
2
3
  import isUnicodeSupported from 'is-unicode-supported';
3
4
  import * as color from 'kleur/colors';
@@ -23,6 +24,7 @@ const S_BAR = s('│', '|');
23
24
 
24
25
  /**
25
26
  * @param {object} opts
27
+ * @param {string} opts.message
26
28
  * @param {string} opts.active
27
29
  * @param {string} opts.inactive
28
30
  * @param {boolean} opts.initialValue
@@ -67,9 +69,9 @@ export const confirm = (opts) => {
67
69
  * @param {string} [opts.placeholder]
68
70
  * @param {string} [opts.defaultValue]
69
71
  * @param {string} [opts.initialValue]
70
- * @param {string} [opts.initialValue]
71
- * @param {function} [opts.validate]
72
- * @returns {Promise<string|symbol>}
72
+ * @param {string} [opts.hint]
73
+ * @param {(value: string | undefined) => any} [opts.validate]
74
+ * @returns {Promise<string | symbol | undefined>}
73
75
  */
74
76
  export const text = (opts) => {
75
77
  const { hint = '' } = opts;
@@ -82,24 +84,36 @@ export const text = (opts) => {
82
84
  render() {
83
85
  const title = `${opts.message}\n`;
84
86
  const placeholder = opts.placeholder
85
- ? color.inverse(opts.placeholder[0]) +
86
- color.dim(opts.placeholder.slice(1))
87
- : color.inverse(color.hidden('_'));
88
- const value = !this.value
89
- ? `${placeholder} ${color.dim(hint)}`.trim()
90
- : this.valueWithCursor;
87
+ ? styleText('inverse', opts.placeholder[0]) +
88
+ styleText('dim', opts.placeholder.slice(1))
89
+ : styleText(['inverse', 'hidden'], '_');
90
+ const userInput = !this.userInput
91
+ ? `${placeholder} ${styleText('dim', hint)}`.trim()
92
+ : this.userInputWithCursor;
93
+ const value = this.value ?? '';
91
94
 
92
95
  switch (this.state) {
93
- case 'error':
94
- return `${title.trim()}\n${value}\n ${color.yellow(this.error)}\n`;
95
- case 'submit':
96
- return `${color.dim(`${title}`)} ${color.green(this.value || opts.placeholder)}\n`;
97
- case 'cancel':
98
- return `${title} ${color.strikethrough(
99
- color.dim(this.value ?? placeholder),
100
- )}\n`;
101
- default:
102
- return `${title} ${value}\n`;
96
+ case 'error': {
97
+ const errorText = this.error
98
+ ? ` ${styleText('yellow', this.error)}`
99
+ : '';
100
+ return `${title.trim()}\n${userInput}\n${errorText}\n`;
101
+ }
102
+ case 'submit': {
103
+ const valueText = value
104
+ ? `${styleText('green', value)}`
105
+ : '';
106
+ return `${styleText('dim', `${title}`)} ${valueText}\n`;
107
+ }
108
+ case 'cancel': {
109
+ const valueText = value
110
+ ? ` ${styleText(['strikethrough', 'dim'], value)}`
111
+ : '';
112
+ return `${title}${valueText}${value.trim() ? `\n` : ''}`;
113
+ }
114
+ default: {
115
+ return `${title}${userInput}\n\n`;
116
+ }
103
117
  }
104
118
  },
105
119
  }).prompt();
@@ -140,6 +154,22 @@ const limitOptions = (params) => {
140
154
  });
141
155
  };
142
156
 
157
+ /**
158
+ *
159
+ * @param {string} label
160
+ * @param {(text: string) => string} format
161
+ * @returns {string}
162
+ */
163
+ const computeLabel = (label, format) => {
164
+ if (!label.includes('\n')) {
165
+ return format(label);
166
+ }
167
+ return label
168
+ .split('\n')
169
+ .map((line) => format(line))
170
+ .join('\n');
171
+ };
172
+
143
173
  /**
144
174
  * @typedef {string|boolean|number} Value
145
175
  */
@@ -149,12 +179,13 @@ const limitOptions = (params) => {
149
179
  * @property {string} value
150
180
  * @property {string} [label]
151
181
  * @property {string} [hint]
182
+ * @property {boolean} [disabled]
152
183
  */
153
184
 
154
185
  /**
155
186
  * @param {object} opts
156
187
  * @param {string} opts.message
157
- * @param {Option<Value>[]} opts.options
188
+ * @param {import('@clack/core').SelectOptions<Value>} opts.options
158
189
  * @param {Value} [opts.initialValue]
159
190
  * @param {number} [opts.maxItems]
160
191
  * @returns {Promise<boolean|symbol>}
@@ -162,47 +193,69 @@ const limitOptions = (params) => {
162
193
  export const select = (opts) => {
163
194
  /**
164
195
  *
165
- * @param {Option<Value>} option
166
- * @param {'inactive' | 'active' | 'selected' | 'cancelled'} state
196
+ * @param {Option} option
197
+ * @param {'inactive' | 'active' | 'selected' | 'cancelled' | 'disabled'} state
167
198
  * @returns {string}
168
199
  */
169
200
  const opt = (option, state) => {
170
201
  const label = option.label ?? String(option.value);
171
202
  switch (state) {
203
+ case 'disabled':
204
+ return `${styleText('gray', S_RADIO_INACTIVE)} ${computeLabel(label, (text) => styleText('gray', text))}${
205
+ option.hint
206
+ ? ` ${styleText('dim', `(${option.hint ?? 'disabled'})`)}`
207
+ : ''
208
+ }`;
172
209
  case 'selected':
173
- return `${label}`;
210
+ return `${computeLabel(label, (text) => text)}`;
174
211
  case 'active':
175
- return `${color.green(S_RADIO_ACTIVE)} ${label} ${
176
- option.hint ? color.dim(`(${option.hint})`) : ''
212
+ return `${styleText('green', S_RADIO_ACTIVE)} ${label}${
213
+ option.hint
214
+ ? ` ${styleText('dim', `(${option.hint})`)}`
215
+ : ''
177
216
  }`;
178
217
  case 'cancelled':
179
- return `${color.strikethrough(color.dim(label))}`;
218
+ return `${computeLabel(label, (str) => styleText(['strikethrough', 'dim'], str))}`;
180
219
  default:
181
- return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`;
220
+ return `${styleText('dim', S_RADIO_INACTIVE)} ${computeLabel(label, (text) => styleText('dim', text))}`;
182
221
  }
183
222
  };
184
223
 
185
224
  return new SelectPrompt({
186
225
  options: opts.options,
226
+ signal: opts.signal,
227
+ input: opts.input,
228
+ output: opts.output,
187
229
  initialValue: opts.initialValue,
188
230
  render() {
189
231
  const title = `${opts.message}\n`;
190
232
 
191
233
  switch (this.state) {
192
- case 'submit':
193
- return `${color.dim(title)} ${color.green(opt(this.options[this.cursor], 'selected'))}\n`;
194
- case 'cancel':
195
- return `${title} ${opt(
196
- this.options[this.cursor],
197
- 'cancelled',
198
- )}\n`;
234
+ case 'submit': {
235
+ return `${styleText('dim', title)} ${styleText('green', opt(this.options[this.cursor], 'selected'))}\n`;
236
+ }
237
+ case 'cancel': {
238
+ return `${title}${opt(this.options[this.cursor], 'cancelled')}`;
239
+ }
199
240
  default: {
200
- return `${title} ${limitOptions({
241
+ const titleLineCount = title.split('\n').length;
242
+ const footerLineCount = 1;
243
+ return `${title}${limitOptions({
244
+ output: opts.output,
201
245
  cursor: this.cursor,
202
246
  options: this.options,
203
247
  maxItems: opts.maxItems,
248
+ columnPadding: 0,
249
+ rowPadding: titleLineCount + footerLineCount,
204
250
  style: (item, active) =>
205
- opt(item, active ? 'active' : 'inactive'),
251
+ opt(
252
+ item,
253
+ item.disabled
254
+ ? 'disabled'
255
+ : active
256
+ ? 'active'
257
+ : 'inactive',
258
+ ),
206
259
  }).join(`\n`)}\n`;
207
260
  }
208
261
  }
package/src/cli/run.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  FRAGMENT_DIRECTORY,
8
8
  } from './createFragmentFile.js';
9
9
  import { getEntries } from './getEntries.js';
10
- import { log, magenta, bold, cyan, red } from './log.js';
10
+ import { log, magenta, bold, cyan } from './log.js';
11
11
  import save from './plugins/save.js';
12
12
  import * as p from './prompts.js';
13
13
  import { prettifyTime } from './utils.js';
@@ -1,13 +1,20 @@
1
+ /**
2
+ *
3
+ * @param {HTMLElement} node
4
+ * @param {() => void} callback
5
+ * @returns
6
+ */
1
7
  export function resize(node, callback) {
2
8
  if (typeof callback !== 'function') return;
3
9
 
10
+ /** @type {ResizeObserver | null} */
4
11
  let observer = new ResizeObserver(callback);
5
12
 
6
13
  observer.observe(node);
7
14
 
8
15
  return {
9
16
  destroy: () => {
10
- observer.disconnect();
17
+ observer?.disconnect();
11
18
  observer = null;
12
19
  },
13
20
  };
@@ -0,0 +1,93 @@
1
+ /**
2
+ *
3
+ * @param {object} options
4
+ * @param {Function} options.onDragStart
5
+ * @param {Function} options.onDrag
6
+ * @param {Function} options.onDragEnd
7
+ * @returns {import('svelte/attachments').Attachment} */
8
+ export function draggable({ onDragStart, onDrag, onDragEnd } = {}) {
9
+ return (node) => {
10
+ let isDragging = false;
11
+ /** @type {MouseEvent | undefined} */
12
+ let eventStart;
13
+ /** @type {DOMRect | undefined} */
14
+ let rect;
15
+ let classNameDragging = 'fragment-dragging';
16
+
17
+ /**
18
+ *
19
+ * @param {MouseEvent} event
20
+ */
21
+ function handleMouseDown(event) {
22
+ isDragging = true;
23
+ eventStart = event;
24
+
25
+ document.addEventListener('mousemove', handleMouseMove);
26
+ document.addEventListener('mouseup', handleMouseUp);
27
+
28
+ document.body.classList.add(classNameDragging);
29
+
30
+ rect = node.getBoundingClientRect();
31
+
32
+ const params = computeDrag(event);
33
+
34
+ onDragStart?.(event, params);
35
+ onDrag?.(event, params);
36
+ }
37
+
38
+ /**
39
+ *
40
+ * @param {MouseEvent} event
41
+ */
42
+ function handleMouseMove(event) {
43
+ onDrag?.(event, computeDrag(event));
44
+ }
45
+
46
+ /**
47
+ *
48
+ * @param {MouseEvent} event
49
+ */
50
+ function handleMouseUp(event) {
51
+ document.body.classList.remove(classNameDragging);
52
+ document.removeEventListener('mousemove', handleMouseMove);
53
+ document.removeEventListener('mouseup', handleMouseUp);
54
+
55
+ isDragging = false;
56
+
57
+ const params = computeDrag(event);
58
+ onDrag?.(event, params);
59
+ onDragEnd?.(event, params);
60
+ }
61
+
62
+ /**
63
+ *
64
+ * @param {MouseEvent} event
65
+ */
66
+ function computeDrag(event) {
67
+ let distanceX = event.clientX - eventStart.clientX;
68
+ let distanceY = event.clientY - eventStart.clientY;
69
+
70
+ let distance = Math.sqrt(
71
+ distanceX * distanceX + distanceY * distanceY,
72
+ );
73
+
74
+ return {
75
+ distanceX,
76
+ distanceY,
77
+ distance,
78
+ isDragging,
79
+ rect,
80
+ node,
81
+ };
82
+ }
83
+
84
+ node.addEventListener('mousedown', handleMouseDown);
85
+
86
+ return () => {
87
+ node.removeEventListener('mousedown', handleMouseDown);
88
+
89
+ rect = undefined;
90
+ eventStart = undefined;
91
+ };
92
+ };
93
+ }
@@ -1,9 +1,53 @@
1
+ /**
2
+ * @typedef {Object} MessagePayload
3
+ * @property {string} event - The event name
4
+ * @property {any} [data] - Optional event data
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} ShaderWarning
9
+ * @property {string} type - Warning type
10
+ * @property {string} importer - File that imported the shader
11
+ * @property {string} message - Warning message
12
+ * @property {Object} location - Location of the warning
13
+ * @property {string} location.lineText - The line of code with the warning
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} ShaderUpdate
18
+ * @property {ShaderWarning[]} [warnings] - Array of shader warnings
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} SketchUpdate
23
+ * @property {string} filepath
24
+ */
25
+
26
+ /**
27
+ * @callback EventCallback
28
+ * @param {any} data - Event data
29
+ * @returns {void}
30
+ */
31
+
32
+ /**
33
+ * @callback UnsubscribeFunction
34
+ * @returns {void}
35
+ */
36
+
1
37
  const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
2
38
  const socketHost = `${location.hostname}:${__FRAGMENT_PORT__}`;
3
39
 
4
- let socket,
5
- listeners = {};
40
+ /** @type {WebSocket | undefined} */
41
+ let socket;
42
+ /** @type {Record<string, EventCallback[]>} */
43
+ let listeners = {};
44
+ let opened = false;
6
45
 
46
+ /**
47
+ * Handle incoming WebSocket message
48
+ * @param {MessagePayload} payload - The message payload
49
+ * @returns {void}
50
+ */
7
51
  function handleMessage(payload) {
8
52
  const { event, data = {} } = payload;
9
53
  const callbacks = listeners[event];
@@ -13,6 +57,12 @@ function handleMessage(payload) {
13
57
  }
14
58
  }
15
59
 
60
+ /**
61
+ * Subscribe to an event
62
+ * @param {string} event - The event name to listen for
63
+ * @param {EventCallback} cb - The callback function
64
+ * @returns {UnsubscribeFunction} Function to unsubscribe
65
+ */
16
66
  function on(event, cb) {
17
67
  if (!listeners[event]) {
18
68
  listeners[event] = [];
@@ -25,6 +75,12 @@ function on(event, cb) {
25
75
  };
26
76
  }
27
77
 
78
+ /**
79
+ * Unsubscribe from an event
80
+ * @param {string} event - The event name
81
+ * @param {EventCallback} cb - The callback function to remove
82
+ * @returns {void}
83
+ */
28
84
  function off(event, cb) {
29
85
  const callbacks = listeners[event];
30
86
 
@@ -35,9 +91,14 @@ function off(event, cb) {
35
91
  }
36
92
  }
37
93
 
38
- let opened = false;
94
+ /**
95
+ * Emit an event to the server
96
+ * @param {string} event - The event name
97
+ * @param {any} data - The data to send
98
+ * @returns {void}
99
+ */
39
100
  function emit(event, data) {
40
- if (opened) {
101
+ if (socket && opened) {
41
102
  socket.send(
42
103
  JSON.stringify({
43
104
  event,
@@ -52,7 +113,7 @@ if (import.meta.hot) {
52
113
 
53
114
  socket = new WebSocket(`${socketProtocol}://${socketHost}`);
54
115
 
55
- socket.addEventListener('message', async (message) => {
116
+ socket.addEventListener('message', (message) => {
56
117
  const { data } = message;
57
118
 
58
119
  handleMessage(JSON.parse(data));
@@ -63,22 +124,33 @@ if (import.meta.hot) {
63
124
  opened = true;
64
125
  });
65
126
 
66
- import.meta.hot.on('sketch-update', (data) => {
67
- console.log(`[fragment] hmr update /${data.filepath}`);
68
- });
127
+ import.meta.hot.on(
128
+ 'sketch-update',
129
+ /** @param {SketchUpdate} sketchUpdate */
130
+ (sketchUpdate) => {
131
+ console.log(`[fragment] hmr update /${sketchUpdate.filepath}`);
132
+ },
133
+ );
69
134
  }
70
135
 
136
+ /**
137
+ * Client API for WebSocket communication
138
+ * @type {{ on: typeof on, off: typeof off, emit: typeof emit }}
139
+ */
71
140
  export const client = { on, off, emit };
72
141
 
73
142
  client.on('shader-update', (shaderUpdates) => {
74
- shaderUpdates.forEach(({ warnings = [] } = {}) => {
75
- if (warnings.length > 0) {
76
- warnings.forEach((warning) => {
77
- const { location } = warning;
78
- console.warn(
79
- `[fragment-plugin-hsr] ${warning.type} ${warning.importer}\n\n ${location.lineText}\n\n${warning.message}`,
80
- );
81
- });
82
- }
83
- });
143
+ shaderUpdates.forEach(
144
+ /** @param {ShaderUpdate} shaderUpdate */
145
+ ({ warnings = [] } = {}) => {
146
+ if (warnings.length > 0) {
147
+ warnings.forEach((warning) => {
148
+ const { location } = warning;
149
+ console.warn(
150
+ `[fragment-plugin-hsr] ${warning.type} ${warning.importer}\n\n ${location.lineText}\n\n${warning.message}`,
151
+ );
152
+ });
153
+ }
154
+ },
155
+ );
84
156
  });
@@ -0,0 +1,46 @@
1
+ <script>
2
+ let { angle = 0 } = $props();
3
+ </script>
4
+
5
+ <svg
6
+ width="16"
7
+ height="16"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ style="--angle: {angle}"
11
+ >
12
+ <path
13
+ stroke="currentColor"
14
+ stroke-linecap="round"
15
+ stroke-linejoin="round"
16
+ stroke-width="1.5"
17
+ d="M18 8H6"
18
+ ></path>
19
+ <path
20
+ stroke="currentColor"
21
+ stroke-linecap="round"
22
+ stroke-linejoin="round"
23
+ stroke-width="1.5"
24
+ d="M10 5L6 8L10 11"
25
+ ></path>
26
+ <path
27
+ stroke="currentColor"
28
+ stroke-linecap="round"
29
+ stroke-linejoin="round"
30
+ stroke-width="1.5"
31
+ d="M6 16H18"
32
+ ></path>
33
+ <path
34
+ stroke="currentColor"
35
+ stroke-linecap="round"
36
+ stroke-linejoin="round"
37
+ stroke-width="1.5"
38
+ d="M14 13L18 16L14 19"
39
+ ></path>
40
+ </svg>
41
+
42
+ <style>
43
+ svg {
44
+ transform: rotate(var(--angle));
45
+ }
46
+ </style>
@@ -2,18 +2,42 @@ import { rendering } from './state/rendering.svelte';
2
2
  import { sketchesManager } from './state/sketches.svelte';
3
3
  import { getContext } from './triggers/shared';
4
4
 
5
+ /**
6
+ * Register a callback to be called before capturing
7
+ * @param {Function} listener - The callback function to execute before capture
8
+ * @param {string} [context] - The sketch context (defaults to current context)
9
+ * @returns {void}
10
+ */
5
11
  export const onBeforeCapture = (listener, context = getContext()) => {
6
12
  sketchesManager.sketches[context]?.onBeforeCapture(listener);
7
13
  };
8
14
 
15
+ /**
16
+ * Register a callback to be called after capturing
17
+ * @param {Function} listener - The callback function to execute after capture
18
+ * @param {string} [context] - The sketch context (defaults to current context)
19
+ * @returns {void}
20
+ */
9
21
  export const onAfterCapture = (listener, context = getContext()) => {
10
22
  sketchesManager.sketches[context]?.onAfterCapture(listener);
11
23
  };
12
24
 
25
+ /**
26
+ * Register a callback to be called before recording
27
+ * @param {Function} listener - The callback function to execute before recording
28
+ * @param {string} [context] - The sketch context (defaults to current context)
29
+ * @returns {void}
30
+ */
13
31
  export const onBeforeRecord = (listener, context = getContext()) => {
14
32
  sketchesManager.sketches[context]?.onBeforeRecord(listener);
15
33
  };
16
34
 
35
+ /**
36
+ * Register a callback to be called after recording
37
+ * @param {Function} listener - The callback function to execute after recording
38
+ * @param {string} [context] - The sketch context (defaults to current context)
39
+ * @returns {void}
40
+ */
17
41
  export const onAfterRecord = (listener, context = getContext()) => {
18
42
  sketchesManager.sketches[context]?.onAfterRecord(listener);
19
43
  };
@@ -23,7 +47,7 @@ export const onAfterRecord = (listener, context = getContext()) => {
23
47
  * @param {object} options
24
48
  * @param {string} [options.filename]
25
49
  * @param {function} [options.pattern]
26
- * @param {exportDir} [options.pattern]
50
+ * @param {string} [options.exportDir]
27
51
  */
28
52
  export async function screenshot({ filename, pattern, exportDir } = {}) {
29
53
  const context = getContext();