@tremolo-ui/dom 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +383 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +269 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +269 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +374 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/file/accept.ts +58 -0
- package/src/file/drop-zone.ts +202 -0
- package/src/index.ts +26 -0
- package/src/input/apply-delta.ts +73 -0
- package/src/input/modifiers.ts +121 -0
- package/src/piano/index.ts +2 -2
- package/src/piano/layout.ts +183 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clamp, linearScale, normalizeValue,
|
|
1
|
+
import { clamp, isBlackKey, isWhiteKey, linearScale, normalizeValue, noteKey, noteKeys, stepValue, toPrecision } from "@tremolo-ui/functions";
|
|
2
2
|
//#region src/canvas/context.ts
|
|
3
3
|
/**
|
|
4
4
|
* The assignable parts of a 2D context's drawing state. They have to be
|
|
@@ -235,6 +235,271 @@ function createAnimationCanvas(canvas, options) {
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
//#endregion
|
|
238
|
+
//#region src/file/accept.ts
|
|
239
|
+
/**
|
|
240
|
+
* Does a file satisfy an `accept` attribute?
|
|
241
|
+
*
|
|
242
|
+
* `accept` is written the way the HTML attribute is: a comma separated list of
|
|
243
|
+
* extensions (`.wav`), MIME types (`audio/wav`) and type groups (`audio/*`).
|
|
244
|
+
* Anything that matches one entry is accepted, and an empty or missing
|
|
245
|
+
* `accept` takes everything.
|
|
246
|
+
*
|
|
247
|
+
* **The browser's own `accept` is only a hint to the file picker.** A person
|
|
248
|
+
* can switch it to "All Files", drag a file in, or pick one the picker was
|
|
249
|
+
* never asked about, so what arrives still has to be checked.
|
|
250
|
+
*
|
|
251
|
+
* A rule that cannot be decided is not treated as a rejection: with no `name`,
|
|
252
|
+
* an extension rule says nothing either way, and `accept=".wav"` reports a
|
|
253
|
+
* match rather than refusing a file it has not seen the name of. The name is
|
|
254
|
+
* there by the time the file is dropped, which is when the answer counts.
|
|
255
|
+
*/
|
|
256
|
+
function matchesAccept(candidate, accept) {
|
|
257
|
+
const rules = (accept ?? "").split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
|
|
258
|
+
if (rules.length === 0) return true;
|
|
259
|
+
const name = candidate.name?.toLowerCase();
|
|
260
|
+
const type = candidate.type.toLowerCase();
|
|
261
|
+
let undecided = false;
|
|
262
|
+
for (const rule of rules) if (rule.startsWith(".")) {
|
|
263
|
+
if (name === void 0) undecided = true;
|
|
264
|
+
else if (name.endsWith(rule)) return true;
|
|
265
|
+
} else if (rule.endsWith("/*")) {
|
|
266
|
+
if (type.startsWith(rule.slice(0, -1))) return true;
|
|
267
|
+
} else if (type === rule) return true;
|
|
268
|
+
return undecided;
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/file/drop-zone.ts
|
|
272
|
+
/**
|
|
273
|
+
* Take files dropped onto an element.
|
|
274
|
+
*
|
|
275
|
+
* ```ts
|
|
276
|
+
* const zone = createDropZone(element, {
|
|
277
|
+
* accept: 'audio/*',
|
|
278
|
+
* onDrop: (files) => load(files[0]),
|
|
279
|
+
* })
|
|
280
|
+
* ```
|
|
281
|
+
*
|
|
282
|
+
* The element needs no attribute of its own: a drop target is made by
|
|
283
|
+
* cancelling `dragover`, which this does.
|
|
284
|
+
*/
|
|
285
|
+
function createDropZone(element, options = {}) {
|
|
286
|
+
let opts = options;
|
|
287
|
+
let state = {
|
|
288
|
+
over: false,
|
|
289
|
+
invalid: false
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* `dragenter` and `dragleave` fire for descendants too, so moving between
|
|
293
|
+
* two children of the zone leaves before it enters. Counting the pairs is
|
|
294
|
+
* what keeps the state from flickering off in the middle of the element.
|
|
295
|
+
*/
|
|
296
|
+
let depth = 0;
|
|
297
|
+
function setState(next) {
|
|
298
|
+
if (next.over === state.over && next.invalid === state.invalid) return;
|
|
299
|
+
state = next;
|
|
300
|
+
opts.onStateChange?.(state);
|
|
301
|
+
}
|
|
302
|
+
function carriesFiles(transfer) {
|
|
303
|
+
return !!transfer?.types.includes("Files");
|
|
304
|
+
}
|
|
305
|
+
/** Whether anything being dragged could still be accepted. */
|
|
306
|
+
function anyAcceptable(transfer) {
|
|
307
|
+
const items = Array.from(transfer?.items ?? []).filter((item) => item.kind === "file");
|
|
308
|
+
if (items.length === 0) return true;
|
|
309
|
+
return items.some((item) => matchesAccept({ type: item.type }, opts.accept));
|
|
310
|
+
}
|
|
311
|
+
function onDragEnter(event) {
|
|
312
|
+
if (!carriesFiles(event.dataTransfer)) return;
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
depth += 1;
|
|
315
|
+
setState({
|
|
316
|
+
over: true,
|
|
317
|
+
invalid: !anyAcceptable(event.dataTransfer)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function onDragOver(event) {
|
|
321
|
+
if (!carriesFiles(event.dataTransfer)) return;
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = opts.disabled || state.invalid ? "none" : "copy";
|
|
324
|
+
if (!state.over) {
|
|
325
|
+
depth = Math.max(depth, 1);
|
|
326
|
+
setState({
|
|
327
|
+
over: true,
|
|
328
|
+
invalid: !anyAcceptable(event.dataTransfer)
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function onDragLeave(event) {
|
|
333
|
+
if (!carriesFiles(event.dataTransfer)) return;
|
|
334
|
+
depth = Math.max(0, depth - 1);
|
|
335
|
+
if (depth === 0) setState({
|
|
336
|
+
over: false,
|
|
337
|
+
invalid: false
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function onDrop(event) {
|
|
341
|
+
if (!carriesFiles(event.dataTransfer)) return;
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
depth = 0;
|
|
344
|
+
setState({
|
|
345
|
+
over: false,
|
|
346
|
+
invalid: false
|
|
347
|
+
});
|
|
348
|
+
if (opts.disabled) return;
|
|
349
|
+
const dropped = Array.from(event.dataTransfer?.files ?? []);
|
|
350
|
+
const accepted = [];
|
|
351
|
+
const rejected = [];
|
|
352
|
+
for (const file of dropped) if (matchesAccept(file, opts.accept)) accepted.push(file);
|
|
353
|
+
else rejected.push(file);
|
|
354
|
+
if (rejected.length > 0) opts.onReject?.(rejected, event);
|
|
355
|
+
const taken = opts.multiple ? accepted : accepted.slice(0, 1);
|
|
356
|
+
if (taken.length > 0) opts.onDrop?.(taken, event);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A drag that ends anywhere else — dropped on another element, cancelled
|
|
360
|
+
* with Esc, taken out of the window — sends no `dragleave` here, and the
|
|
361
|
+
* element would stay marked as a drop target for good.
|
|
362
|
+
*/
|
|
363
|
+
function onDragEndAnywhere() {
|
|
364
|
+
depth = 0;
|
|
365
|
+
setState({
|
|
366
|
+
over: false,
|
|
367
|
+
invalid: false
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const handlers = {
|
|
371
|
+
dragenter: onDragEnter,
|
|
372
|
+
dragover: onDragOver,
|
|
373
|
+
dragleave: onDragLeave,
|
|
374
|
+
drop: onDrop
|
|
375
|
+
};
|
|
376
|
+
for (const [type, handler] of Object.entries(handlers)) element.addEventListener(type, handler);
|
|
377
|
+
const doc = element.ownerDocument;
|
|
378
|
+
doc?.addEventListener("dragend", onDragEndAnywhere);
|
|
379
|
+
doc?.addEventListener("drop", onDragEndAnywhere);
|
|
380
|
+
return {
|
|
381
|
+
get state() {
|
|
382
|
+
return state;
|
|
383
|
+
},
|
|
384
|
+
update: (next) => {
|
|
385
|
+
opts = {
|
|
386
|
+
...opts,
|
|
387
|
+
...next
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
destroy: () => {
|
|
391
|
+
for (const [type, handler] of Object.entries(handlers)) element.removeEventListener(type, handler);
|
|
392
|
+
doc?.removeEventListener("dragend", onDragEndAnywhere);
|
|
393
|
+
doc?.removeEventListener("drop", onDragEndAnywhere);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/input/modifiers.ts
|
|
399
|
+
/**
|
|
400
|
+
* Checked in this order, and the first one that is both held and configured
|
|
401
|
+
* wins. Fixing an order is what keeps two modifiers held at once from
|
|
402
|
+
* behaving differently between browsers.
|
|
403
|
+
*/
|
|
404
|
+
const MODIFIER_ORDER = [
|
|
405
|
+
"meta",
|
|
406
|
+
"ctrl",
|
|
407
|
+
"alt",
|
|
408
|
+
"shift"
|
|
409
|
+
];
|
|
410
|
+
const MODIFIER_FLAG = {
|
|
411
|
+
meta: "metaKey",
|
|
412
|
+
ctrl: "ctrlKey",
|
|
413
|
+
alt: "altKey",
|
|
414
|
+
shift: "shiftKey"
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* A map is the only form with a `default` key, which is what tells it apart
|
|
418
|
+
* from a bare setting. Tuples are arrays, so they never match.
|
|
419
|
+
*/
|
|
420
|
+
function isModifierMap(value) {
|
|
421
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "default" in value;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Pick the setting that applies, given the modifier keys being held.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* selectModifier({ default: 1, shift: 0.1 }, event)
|
|
428
|
+
*/
|
|
429
|
+
function selectModifier(options, modifiers) {
|
|
430
|
+
if (!isModifierMap(options)) return {
|
|
431
|
+
value: options,
|
|
432
|
+
modifier: null
|
|
433
|
+
};
|
|
434
|
+
if (modifiers) for (const modifier of MODIFIER_ORDER) {
|
|
435
|
+
const value = options[modifier];
|
|
436
|
+
if (value !== void 0 && modifiers[MODIFIER_FLAG[modifier]]) return {
|
|
437
|
+
value,
|
|
438
|
+
modifier
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
value: options.default,
|
|
443
|
+
modifier: null
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Turn every entry of a setting into another kind of setting, keeping which
|
|
448
|
+
* modifier each belongs to.
|
|
449
|
+
*
|
|
450
|
+
* A drag sensitivity is a number and a keyboard amount is a tuple, but the two
|
|
451
|
+
* describe the same thing from the caller's side. This carries one over to the
|
|
452
|
+
* other so that a component can hand a sensitivity to {@link applyDelta}
|
|
453
|
+
* without unpicking the modifier map itself — which matters, since naming a
|
|
454
|
+
* modifier is also what takes `step` out of the pipeline.
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* mapModifier({ default: 1, shift: 0.1 }, (f) => ['raw', step * f])
|
|
458
|
+
* // { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
459
|
+
*/
|
|
460
|
+
function mapModifier(options, fn) {
|
|
461
|
+
if (!isModifierMap(options)) return fn(options);
|
|
462
|
+
const mapped = { default: fn(options.default) };
|
|
463
|
+
for (const modifier of MODIFIER_ORDER) {
|
|
464
|
+
const value = options[modifier];
|
|
465
|
+
if (value !== void 0) mapped[modifier] = fn(value);
|
|
466
|
+
}
|
|
467
|
+
return mapped;
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/input/apply-delta.ts
|
|
471
|
+
/**
|
|
472
|
+
* Move a value by an amount of input, as reported by a wheel or an arrow key.
|
|
473
|
+
*
|
|
474
|
+
* The pipeline matches {@link createDragValue}: scale, then step, then clamp.
|
|
475
|
+
* Which key or which sign of `deltaY` counts as which direction is left to the
|
|
476
|
+
* caller, since it differs per component.
|
|
477
|
+
*
|
|
478
|
+
* @param direction which way, and how many times, to apply the option. The
|
|
479
|
+
* size of one step is `option[1]`, so this is normally `1` or `-1`.
|
|
480
|
+
*
|
|
481
|
+
* @param modifiers the event, for `options` that name a modifier key. See
|
|
482
|
+
* {@link selectModifier}.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* // ArrowDown on a slider whose keyboard option is ['raw', 1]
|
|
486
|
+
* applyDelta(value, -1, keyboard, { min, max, step, scale })
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* // Shift+ArrowDown, where `keyboard` is { default: …, shift: ['raw', 0.1] }
|
|
490
|
+
* applyDelta(value, -1, keyboard, range, event)
|
|
491
|
+
*/
|
|
492
|
+
function applyDelta(value, direction, options, { min, max, step, scale = linearScale }, modifiers) {
|
|
493
|
+
if (min >= max) throw new RangeError("requirements: min < max");
|
|
494
|
+
if (step !== void 0 && (!Number.isFinite(step) || step <= 0)) throw new RangeError("applyDelta step: requirements: finite and greater than 0");
|
|
495
|
+
const { value: [mode, amount], modifier } = selectModifier(options, modifiers);
|
|
496
|
+
const x = direction * amount;
|
|
497
|
+
const next = mode === "normalized" ? scale.denormalize(scale.normalize(value, min, max) + x, min, max) : value + x;
|
|
498
|
+
const quantum = modifier === null ? step : void 0;
|
|
499
|
+
const stepped = quantum !== void 0 ? stepValue(next, quantum) : next;
|
|
500
|
+
return clamp(toPrecision(stepped), min, max);
|
|
501
|
+
}
|
|
502
|
+
//#endregion
|
|
238
503
|
//#region src/midi/access.ts
|
|
239
504
|
/** @private */
|
|
240
505
|
const PERMISSION_DENIED = "PERMISSION_DENIED";
|
|
@@ -448,6 +713,113 @@ function createMIDIInput(midiAccess, handlers) {
|
|
|
448
713
|
};
|
|
449
714
|
}
|
|
450
715
|
//#endregion
|
|
716
|
+
//#region src/piano/layout.ts
|
|
717
|
+
/**
|
|
718
|
+
* `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
|
|
719
|
+
*/
|
|
720
|
+
function getNoteRangeArray(noteRange) {
|
|
721
|
+
return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
|
|
722
|
+
}
|
|
723
|
+
const DEFAULT_KEY_GAP = 1;
|
|
724
|
+
const DEFAULT_BLACK_KEY_WIDTH_RATIO = .65;
|
|
725
|
+
const DEFAULT_BLACK_KEY_HEIGHT_RATIO = .6;
|
|
726
|
+
/**
|
|
727
|
+
* How many white keys sit at or before each pitch class, counting from C.
|
|
728
|
+
*
|
|
729
|
+
* A black key shares the number of the white key to its left plus one, which
|
|
730
|
+
* puts it on the boundary between the two; {@link notePosition} then shifts it
|
|
731
|
+
* back by half its width to centre it there.
|
|
732
|
+
*/
|
|
733
|
+
const whiteKeysBefore = {
|
|
734
|
+
C: 0,
|
|
735
|
+
"C#": 1,
|
|
736
|
+
D: 1,
|
|
737
|
+
"D#": 2,
|
|
738
|
+
E: 2,
|
|
739
|
+
F: 3,
|
|
740
|
+
"F#": 4,
|
|
741
|
+
G: 4,
|
|
742
|
+
"G#": 5,
|
|
743
|
+
A: 5,
|
|
744
|
+
"A#": 6,
|
|
745
|
+
B: 6
|
|
746
|
+
};
|
|
747
|
+
/** Width of a black key in pixels. */
|
|
748
|
+
function blackKeyWidth(layout) {
|
|
749
|
+
return layout.whiteKeyWidth * (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO);
|
|
750
|
+
}
|
|
751
|
+
function rawNotePosition(note, layout) {
|
|
752
|
+
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
753
|
+
const target = noteKey(note);
|
|
754
|
+
const first = noteKey(layout.noteRange.first);
|
|
755
|
+
const octave = Math.floor((note - layout.noteRange.first) / 12);
|
|
756
|
+
const octaveOffset = noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0;
|
|
757
|
+
const whiteKeysIn = whiteKeysBefore[target] - whiteKeysBefore[first] + (octave + octaveOffset) * 7;
|
|
758
|
+
return isBlackKey(note) ? whiteKeysIn * slot - blackKeyWidth(layout) / 2 : whiteKeysIn * slot;
|
|
759
|
+
}
|
|
760
|
+
function pianoBounds(layout) {
|
|
761
|
+
const notes = getNoteRangeArray(layout.noteRange);
|
|
762
|
+
if (notes.length === 0) return {
|
|
763
|
+
left: 0,
|
|
764
|
+
right: 0
|
|
765
|
+
};
|
|
766
|
+
let left = Infinity;
|
|
767
|
+
let right = -Infinity;
|
|
768
|
+
const whiteWidth = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
769
|
+
for (const note of notes) {
|
|
770
|
+
const noteLeft = rawNotePosition(note, layout);
|
|
771
|
+
const width = isBlackKey(note) ? blackKeyWidth(layout) : whiteWidth;
|
|
772
|
+
left = Math.min(left, noteLeft);
|
|
773
|
+
right = Math.max(right, noteLeft + width);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
left,
|
|
777
|
+
right
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/** Width of the whole keyboard in pixels. */
|
|
781
|
+
function pianoWidth(layout) {
|
|
782
|
+
const { left, right } = pianoBounds(layout);
|
|
783
|
+
return right - left;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Offset of the left edge of a key from the left edge of the keyboard, in
|
|
787
|
+
* pixels.
|
|
788
|
+
*
|
|
789
|
+
* Notes outside `noteRange` are placed too, so the value is negative below
|
|
790
|
+
* `noteRange.first`.
|
|
791
|
+
*/
|
|
792
|
+
function notePosition(note, layout) {
|
|
793
|
+
return rawNotePosition(note, layout) - pianoBounds(layout).left;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* The note drawn at a point, or null where there is none.
|
|
797
|
+
*
|
|
798
|
+
* Black keys are tested first, so they win where they overlap a white one. A
|
|
799
|
+
* white key covers its gap as well as its width, so the whole width of the
|
|
800
|
+
* keyboard belongs to some key and a click cannot fall between two.
|
|
801
|
+
*
|
|
802
|
+
* @param x offset from the left edge of the keyboard, in pixels
|
|
803
|
+
* @param y offset from its top edge, in pixels
|
|
804
|
+
* @param height height of the keyboard, in pixels
|
|
805
|
+
*/
|
|
806
|
+
function noteAt(x, y, height, layout) {
|
|
807
|
+
if (x < 0 || x >= pianoWidth(layout) || y < 0 || y >= height) return null;
|
|
808
|
+
const notes = getNoteRangeArray(layout.noteRange);
|
|
809
|
+
if (y < height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)) for (const note of notes) {
|
|
810
|
+
if (isWhiteKey(note)) continue;
|
|
811
|
+
const left = notePosition(note, layout);
|
|
812
|
+
if (left <= x && x < left + blackKeyWidth(layout)) return note;
|
|
813
|
+
}
|
|
814
|
+
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
815
|
+
for (const note of notes) {
|
|
816
|
+
if (isBlackKey(note)) continue;
|
|
817
|
+
const left = notePosition(note, layout);
|
|
818
|
+
if (left <= x && x < left + slot) return note;
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
451
823
|
//#region src/pointer/drag.ts
|
|
452
824
|
/**
|
|
453
825
|
* Applied to the element for the lifetime of the instance.
|
|
@@ -1064,6 +1436,6 @@ function createSelectionBox(options) {
|
|
|
1064
1436
|
};
|
|
1065
1437
|
}
|
|
1066
1438
|
//#endregion
|
|
1067
|
-
export { NOT_SUPPORTED, PERMISSION_DENIED, PITCH_BEND_CENTER, UNAVAILABLE, createAnimationCanvas, createDrag, createDragValue, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createSelectionBox, createWheel, drawingState, elementMapping, isDrawingState, relativeMapping, selectionBoxCovers, toXY };
|
|
1439
|
+
export { NOT_SUPPORTED, PERMISSION_DENIED, PITCH_BEND_CENTER, UNAVAILABLE, applyDelta, blackKeyWidth, createAnimationCanvas, createDrag, createDragValue, createDropZone, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createSelectionBox, createWheel, drawingState, elementMapping, getNoteRangeArray, isDrawingState, mapModifier, matchesAccept, noteAt, notePosition, pianoWidth, relativeMapping, selectModifier, selectionBoxCovers, toXY };
|
|
1068
1440
|
|
|
1069
1441
|
//# sourceMappingURL=index.js.map
|