@patchbayhq/svelte 0.1.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/README.md +20 -0
- package/package.json +57 -0
- package/src/Arrows.svelte +22 -0
- package/src/Button.svelte +52 -0
- package/src/ClipStepLauncher.svelte +105 -0
- package/src/ColorSwatches.svelte +23 -0
- package/src/Dial.svelte +68 -0
- package/src/Drop.svelte +25 -0
- package/src/Envelope.svelte +124 -0
- package/src/Gain.svelte +53 -0
- package/src/Grid.svelte +56 -0
- package/src/Label.svelte +13 -0
- package/src/Line.svelte +18 -0
- package/src/MacroRack.svelte +48 -0
- package/src/Menu.svelte +139 -0
- package/src/Meter.svelte +21 -0
- package/src/NumberBox.svelte +34 -0
- package/src/Panel.svelte +13 -0
- package/src/PanelHeader.svelte +28 -0
- package/src/PanelSection.svelte +23 -0
- package/src/Scope.svelte +50 -0
- package/src/Slider.svelte +55 -0
- package/src/StatusIndicator.svelte +26 -0
- package/src/StepSequencer.svelte +77 -0
- package/src/Tabs.svelte +33 -0
- package/src/TextButton.svelte +54 -0
- package/src/Toggle.svelte +32 -0
- package/src/composites.ts +35 -0
- package/src/index.ts +69 -0
- package/src/shared.ts +29 -0
- package/src/styles.css +1 -0
- package/src/tailwind.css +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @patchbayhq/svelte
|
|
2
|
+
|
|
3
|
+
Svelte wrappers for patchbay UI audio controls.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @patchbayhq/svelte
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```svelte
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import "@patchbayhq/svelte/styles.css";
|
|
12
|
+
import { Slider } from "@patchbayhq/svelte";
|
|
13
|
+
|
|
14
|
+
let value = 0.42;
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<Slider label="Filter" bind:value modulation={0.64} />
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Docs: https://ui.patchbay.tools
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patchbayhq/svelte",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Svelte wrappers for patchbay UI audio controls.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"svelte": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"homepage": "https://ui.patchbay.tools",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/monadoid/patchbay-ui.git",
|
|
13
|
+
"directory": "packages/svelte"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/monadoid/patchbay-ui/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"audio",
|
|
20
|
+
"controls",
|
|
21
|
+
"svelte",
|
|
22
|
+
"ui"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"./src/styles.css",
|
|
30
|
+
"./src/tailwind.css"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"svelte": "./src/index.ts",
|
|
38
|
+
"types": "./src/index.ts",
|
|
39
|
+
"default": "./src/index.ts"
|
|
40
|
+
},
|
|
41
|
+
"./styles.css": "./src/styles.css",
|
|
42
|
+
"./tailwind.css": "./src/tailwind.css"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"check-types": "svelte-check --tsconfig tsconfig.json"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@patchbayhq/ui": "^0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"svelte": "^5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"svelte": "^5.0.0",
|
|
55
|
+
"svelte-check": "^4.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseComponentProps, type ArrowDirection } from "@patchbayhq/ui";
|
|
3
|
+
|
|
4
|
+
export let value: ArrowDirection = "right";
|
|
5
|
+
export let disabled = false;
|
|
6
|
+
export let onValueChange: ((value: ArrowDirection) => void) | undefined = undefined;
|
|
7
|
+
|
|
8
|
+
let className = "";
|
|
9
|
+
export { className as class };
|
|
10
|
+
|
|
11
|
+
$: props = parseComponentProps("arrows", { disabled, value });
|
|
12
|
+
|
|
13
|
+
function select(nextValue: ArrowDirection) {
|
|
14
|
+
value = nextValue;
|
|
15
|
+
onValueChange?.(value);
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class={["arrows", className].filter(Boolean).join(" ")} role="group">
|
|
20
|
+
<button aria-label="Left" aria-pressed={props.value === "left"} disabled={props.disabled} type="button" on:click={() => select("left")}></button>
|
|
21
|
+
<button aria-label="Right" aria-pressed={props.value === "right"} disabled={props.disabled} type="button" on:click={() => select("right")}></button>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
parseComponentProps,
|
|
4
|
+
type ButtonMode,
|
|
5
|
+
type ControlAppearance,
|
|
6
|
+
} from "@patchbayhq/ui";
|
|
7
|
+
|
|
8
|
+
export let label = "Button";
|
|
9
|
+
export let pressed = false;
|
|
10
|
+
export let mode: ButtonMode = "momentary";
|
|
11
|
+
export let appearance: ControlAppearance = "default";
|
|
12
|
+
export let disabled = false;
|
|
13
|
+
export let onPressedChange: ((pressed: boolean) => void) | undefined = undefined;
|
|
14
|
+
|
|
15
|
+
let className = "";
|
|
16
|
+
export { className as class };
|
|
17
|
+
|
|
18
|
+
$: props = parseComponentProps("button", {
|
|
19
|
+
appearance,
|
|
20
|
+
disabled,
|
|
21
|
+
label,
|
|
22
|
+
mode,
|
|
23
|
+
pressed,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function setPressed(nextPressed: boolean) {
|
|
27
|
+
if (props.disabled) return;
|
|
28
|
+
pressed = nextPressed;
|
|
29
|
+
onPressedChange?.(pressed);
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<button
|
|
34
|
+
aria-label={props.label}
|
|
35
|
+
aria-pressed={props.pressed}
|
|
36
|
+
class={["button", props.pressed ? "is-active" : "", className].filter(Boolean).join(" ")}
|
|
37
|
+
data-appearance={props.appearance}
|
|
38
|
+
{disabled}
|
|
39
|
+
type="button"
|
|
40
|
+
on:click={() => {
|
|
41
|
+
if (props.mode === "toggle") setPressed(!props.pressed);
|
|
42
|
+
}}
|
|
43
|
+
on:pointerdown={() => {
|
|
44
|
+
if (props.mode === "momentary") setPressed(true);
|
|
45
|
+
}}
|
|
46
|
+
on:pointerleave={() => {
|
|
47
|
+
if (props.mode === "momentary") setPressed(false);
|
|
48
|
+
}}
|
|
49
|
+
on:pointerup={() => {
|
|
50
|
+
if (props.mode === "momentary") setPressed(false);
|
|
51
|
+
}}
|
|
52
|
+
></button>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
StepChangeDetail,
|
|
4
|
+
StepKey,
|
|
5
|
+
StepLoop,
|
|
6
|
+
StepNote,
|
|
7
|
+
} from "@patchbayhq/ui";
|
|
8
|
+
import StepSequencer from "./StepSequencer.svelte";
|
|
9
|
+
import Panel from "./Panel.svelte";
|
|
10
|
+
import PanelHeader from "./PanelHeader.svelte";
|
|
11
|
+
import StatusIndicator from "./StatusIndicator.svelte";
|
|
12
|
+
import type { StatusTone } from "./composites";
|
|
13
|
+
|
|
14
|
+
export let activeKey: StepKey = "C4";
|
|
15
|
+
export let bars = 4;
|
|
16
|
+
export let clipName = "Pattern";
|
|
17
|
+
export let disabled = false;
|
|
18
|
+
export let launchLabel = "Launch";
|
|
19
|
+
export let loop: StepLoop = { start: 1, end: 16 };
|
|
20
|
+
export let notes: StepNote[] = [];
|
|
21
|
+
export let onCellChange:
|
|
22
|
+
| ((change: Extract<StepChangeDetail, { type: "cell" }>) => void)
|
|
23
|
+
| undefined = undefined;
|
|
24
|
+
export let onChange: ((detail: StepChangeDetail) => void) | undefined = undefined;
|
|
25
|
+
export let onKeyChange:
|
|
26
|
+
| ((change: Extract<StepChangeDetail, { type: "key" }>) => void)
|
|
27
|
+
| undefined = undefined;
|
|
28
|
+
export let onKeyPress: ((key: StepKey) => void) | undefined = undefined;
|
|
29
|
+
export let onKeyRelease: ((key: StepKey) => void) | undefined = undefined;
|
|
30
|
+
export let onLaunch: (() => void) | undefined = undefined;
|
|
31
|
+
export let onLoopChange: ((loop: StepLoop) => void) | undefined = undefined;
|
|
32
|
+
export let onNotesChange: ((notes: StepNote[]) => void) | undefined = undefined;
|
|
33
|
+
export let onStop: (() => void) | undefined = undefined;
|
|
34
|
+
export let playing = false;
|
|
35
|
+
export let statusLabel = "Clip";
|
|
36
|
+
export let statusTone: StatusTone | undefined = undefined;
|
|
37
|
+
export let statusValue: string | undefined = undefined;
|
|
38
|
+
export let stopLabel = "Stop";
|
|
39
|
+
export let title = "Step launcher";
|
|
40
|
+
|
|
41
|
+
let className = "";
|
|
42
|
+
export { className as class };
|
|
43
|
+
|
|
44
|
+
$: resolvedStatusTone = statusTone ?? (playing ? "active" : "idle");
|
|
45
|
+
$: resolvedStatusValue = statusValue ?? (playing ? "Playing" : "Ready");
|
|
46
|
+
|
|
47
|
+
function handleLaunch() {
|
|
48
|
+
onLaunch?.();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleStop() {
|
|
52
|
+
onStop?.();
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<Panel class={["clip-step-launcher", className].filter(Boolean).join(" ")}>
|
|
57
|
+
<PanelHeader {title} subtitle={clipName}>
|
|
58
|
+
<StatusIndicator
|
|
59
|
+
label={statusLabel}
|
|
60
|
+
pulse={playing}
|
|
61
|
+
tone={resolvedStatusTone}
|
|
62
|
+
value={resolvedStatusValue}
|
|
63
|
+
/>
|
|
64
|
+
</PanelHeader>
|
|
65
|
+
|
|
66
|
+
<div class="clip-step-launcher__transport">
|
|
67
|
+
<button
|
|
68
|
+
class="text-button clip-step-launcher__launch"
|
|
69
|
+
data-appearance="primary"
|
|
70
|
+
{disabled}
|
|
71
|
+
on:click={handleLaunch}
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
<span class="text-button__label">{launchLabel}</span>
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
class="text-button clip-step-launcher__stop"
|
|
78
|
+
data-appearance="subtle"
|
|
79
|
+
{disabled}
|
|
80
|
+
on:click={handleStop}
|
|
81
|
+
type="button"
|
|
82
|
+
>
|
|
83
|
+
<span class="text-button__label">{stopLabel}</span>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<StepSequencer
|
|
88
|
+
class="clip-step-launcher__steps"
|
|
89
|
+
bind:activeKey
|
|
90
|
+
bind:loop
|
|
91
|
+
bind:notes
|
|
92
|
+
{bars}
|
|
93
|
+
{onCellChange}
|
|
94
|
+
{onChange}
|
|
95
|
+
{onKeyChange}
|
|
96
|
+
{onKeyPress}
|
|
97
|
+
{onKeyRelease}
|
|
98
|
+
{onLoopChange}
|
|
99
|
+
{onNotesChange}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<div class="clip-step-launcher__extra">
|
|
103
|
+
<slot />
|
|
104
|
+
</div>
|
|
105
|
+
</Panel>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseComponentProps } from "@patchbayhq/ui";
|
|
3
|
+
|
|
4
|
+
export let swatches = ["#9cd8ca", "#f0b51d", "#aebaff"];
|
|
5
|
+
export let value: string | undefined = undefined;
|
|
6
|
+
export let onValueChange: ((value: string) => void) | undefined = undefined;
|
|
7
|
+
|
|
8
|
+
let className = "";
|
|
9
|
+
export { className as class };
|
|
10
|
+
|
|
11
|
+
$: props = parseComponentProps("color-swatches", { swatches, value });
|
|
12
|
+
|
|
13
|
+
function select(nextValue: string) {
|
|
14
|
+
value = nextValue;
|
|
15
|
+
onValueChange?.(nextValue);
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class={["color-swatches", className].filter(Boolean).join(" ")} aria-label="Color swatches">
|
|
20
|
+
{#each props.swatches as swatch, index (`${swatch}-${index}`)}
|
|
21
|
+
<input aria-label={`Color ${index + 1}`} type="color" value={props.value === swatch ? props.value : swatch} on:input={(event) => select((event.currentTarget as HTMLInputElement).value)} />
|
|
22
|
+
{/each}
|
|
23
|
+
</div>
|
package/src/Dial.svelte
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import {
|
|
4
|
+
initDials,
|
|
5
|
+
parseComponentProps,
|
|
6
|
+
type DialDragAxis,
|
|
7
|
+
type DialMode,
|
|
8
|
+
} from "@patchbayhq/ui";
|
|
9
|
+
import { ratio } from "./shared";
|
|
10
|
+
|
|
11
|
+
export let label = "Dial";
|
|
12
|
+
export let value = 0;
|
|
13
|
+
export let min = 0;
|
|
14
|
+
export let max = 1;
|
|
15
|
+
export let step: number | "any" = 0.01;
|
|
16
|
+
export let mode: DialMode = "unipolar";
|
|
17
|
+
export let dragAxis: DialDragAxis = "vertical";
|
|
18
|
+
export let disabled = false;
|
|
19
|
+
export let onValueChange: ((value: number) => void) | undefined = undefined;
|
|
20
|
+
|
|
21
|
+
let className = "";
|
|
22
|
+
let root: HTMLSpanElement | undefined;
|
|
23
|
+
export { className as class };
|
|
24
|
+
|
|
25
|
+
onMount(() => {
|
|
26
|
+
if (root) initDials(root);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
$: props = parseComponentProps("dial", {
|
|
30
|
+
disabled,
|
|
31
|
+
dragAxis,
|
|
32
|
+
label,
|
|
33
|
+
max,
|
|
34
|
+
min,
|
|
35
|
+
mode,
|
|
36
|
+
step,
|
|
37
|
+
value,
|
|
38
|
+
});
|
|
39
|
+
$: valueRatio = ratio(props.value, props.min, props.max);
|
|
40
|
+
$: if (root) initDials(root);
|
|
41
|
+
|
|
42
|
+
function handleInput(event: Event) {
|
|
43
|
+
value = Number((event.currentTarget as HTMLInputElement).value);
|
|
44
|
+
onValueChange?.(value);
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<label class={["field", className].filter(Boolean).join(" ")}>
|
|
49
|
+
<span class="field__label">{props.label}</span>
|
|
50
|
+
<span
|
|
51
|
+
bind:this={root}
|
|
52
|
+
class="dial"
|
|
53
|
+
data-dial
|
|
54
|
+
data-drag-axis={props.dragAxis}
|
|
55
|
+
style={`--dial-value: ${valueRatio}`}
|
|
56
|
+
>
|
|
57
|
+
<input
|
|
58
|
+
aria-label={props.label}
|
|
59
|
+
type="range"
|
|
60
|
+
min={props.min}
|
|
61
|
+
max={props.max}
|
|
62
|
+
step={props.step}
|
|
63
|
+
bind:value
|
|
64
|
+
{disabled}
|
|
65
|
+
on:input={handleInput}
|
|
66
|
+
/>
|
|
67
|
+
</span>
|
|
68
|
+
</label>
|
package/src/Drop.svelte
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseComponentProps } from "@patchbayhq/ui";
|
|
3
|
+
|
|
4
|
+
export let label = "Drop Something Here!";
|
|
5
|
+
export let accept: string | string[] | undefined = undefined;
|
|
6
|
+
export let multiple = false;
|
|
7
|
+
export let disabled = false;
|
|
8
|
+
export let onFilesChange: ((files: FileList) => void) | undefined = undefined;
|
|
9
|
+
|
|
10
|
+
let className = "";
|
|
11
|
+
export { className as class };
|
|
12
|
+
|
|
13
|
+
$: props = parseComponentProps("drop", { accept, disabled, label, multiple });
|
|
14
|
+
$: acceptValue = Array.isArray(props.accept) ? props.accept.join(",") : props.accept;
|
|
15
|
+
|
|
16
|
+
function handleChange(event: Event) {
|
|
17
|
+
const files = (event.currentTarget as HTMLInputElement).files;
|
|
18
|
+
if (files) onFilesChange?.(files);
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<label class={["drop", className].filter(Boolean).join(" ")} title={props.label}>
|
|
23
|
+
<input aria-label={props.label} accept={acceptValue} disabled={props.disabled} multiple={props.multiple} type="file" on:change={handleChange} />
|
|
24
|
+
<span aria-hidden="true"></span>
|
|
25
|
+
</label>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from "svelte";
|
|
3
|
+
import { parseComponentProps, type EnvelopeShape } from "@patchbayhq/ui";
|
|
4
|
+
|
|
5
|
+
export let envelope: EnvelopeShape = { attack: 0.2, decay: 0.3, release: 0.35, sustain: 0.7 };
|
|
6
|
+
export let disabled = false;
|
|
7
|
+
export let onEnvelopeChange: ((envelope: EnvelopeShape) => void) | undefined = undefined;
|
|
8
|
+
|
|
9
|
+
let className = "";
|
|
10
|
+
let dragCleanup: (() => void) | undefined;
|
|
11
|
+
let svg: SVGSVGElement | undefined;
|
|
12
|
+
export { className as class };
|
|
13
|
+
|
|
14
|
+
$: props = parseComponentProps("envelope", { disabled, envelope });
|
|
15
|
+
$: attackX = 8 + props.envelope.attack * 54;
|
|
16
|
+
$: decayX = attackX + props.envelope.decay * 70;
|
|
17
|
+
$: sustainY = 88 - props.envelope.sustain * 76;
|
|
18
|
+
$: releaseX = 194 - props.envelope.release * 40;
|
|
19
|
+
$: path = `M8 88 L8 12 L${attackX.toFixed(1)} 12 L${decayX.toFixed(1)} ${sustainY.toFixed(
|
|
20
|
+
1,
|
|
21
|
+
)} C${releaseX.toFixed(1)} ${sustainY.toFixed(1)} 172 75 194 88`;
|
|
22
|
+
|
|
23
|
+
onDestroy(() => {
|
|
24
|
+
dragCleanup?.();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function setSustainFromClientY(clientY: number) {
|
|
28
|
+
if (!onEnvelopeChange || props.disabled || !svg) return;
|
|
29
|
+
const rect = svg.getBoundingClientRect();
|
|
30
|
+
const sustain = Math.max(0, Math.min(1, 1 - (clientY - rect.top - 12) / 76));
|
|
31
|
+
|
|
32
|
+
envelope = {
|
|
33
|
+
...props.envelope,
|
|
34
|
+
sustain,
|
|
35
|
+
};
|
|
36
|
+
onEnvelopeChange(envelope);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function beginSustainDrag(event: PointerEvent) {
|
|
40
|
+
if (!onEnvelopeChange || props.disabled) return;
|
|
41
|
+
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
dragCleanup?.();
|
|
44
|
+
setSustainFromClientY(event.clientY);
|
|
45
|
+
|
|
46
|
+
const pointerId = event.pointerId;
|
|
47
|
+
const target = event.currentTarget as SVGElement;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
target.setPointerCapture(pointerId);
|
|
51
|
+
} catch {
|
|
52
|
+
// Some browser/SVG combinations skip capture after a quick pointer cancel.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handleMove = (moveEvent: PointerEvent) => {
|
|
56
|
+
if (moveEvent.pointerId !== pointerId) return;
|
|
57
|
+
|
|
58
|
+
moveEvent.preventDefault();
|
|
59
|
+
setSustainFromClientY(moveEvent.clientY);
|
|
60
|
+
};
|
|
61
|
+
const handleEnd = (endEvent: PointerEvent) => {
|
|
62
|
+
if (endEvent.pointerId !== pointerId) return;
|
|
63
|
+
|
|
64
|
+
dragCleanup?.();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
dragCleanup = () => {
|
|
68
|
+
window.removeEventListener("pointermove", handleMove);
|
|
69
|
+
window.removeEventListener("pointerup", handleEnd);
|
|
70
|
+
window.removeEventListener("pointercancel", handleEnd);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
target.releasePointerCapture(pointerId);
|
|
74
|
+
} catch {
|
|
75
|
+
// Capture may already be released by the browser.
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
dragCleanup = undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
window.addEventListener("pointermove", handleMove);
|
|
82
|
+
window.addEventListener("pointerup", handleEnd);
|
|
83
|
+
window.addEventListener("pointercancel", handleEnd);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleSustainKey(event: KeyboardEvent) {
|
|
87
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
envelope = { ...props.envelope, sustain: Math.max(0, Math.min(1, props.envelope.sustain + 0.05)) };
|
|
90
|
+
onEnvelopeChange?.(envelope);
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<div class={["envelope", className].filter(Boolean).join(" ")}>
|
|
95
|
+
<svg bind:this={svg} data-envelope viewBox="0 0 210 96" role="img" aria-label="ADSR envelope">
|
|
96
|
+
<path class="envelope__grid" d="M1 24H209M1 48H209M1 72H209M42 1V95M84 1V95M126 1V95M168 1V95" />
|
|
97
|
+
<path class="envelope__curve" data-envelope-curve d={path} />
|
|
98
|
+
<rect data-envelope-handle="start" x="4" y="84" width="8" height="8" />
|
|
99
|
+
<rect data-envelope-handle="attack" x={attackX - 4} y="8" width="8" height="8" />
|
|
100
|
+
<circle
|
|
101
|
+
data-envelope-handle="decay"
|
|
102
|
+
cx={decayX}
|
|
103
|
+
cy={sustainY}
|
|
104
|
+
r="4"
|
|
105
|
+
role="button"
|
|
106
|
+
tabindex="0"
|
|
107
|
+
aria-label="Adjust decay"
|
|
108
|
+
on:pointerdown={beginSustainDrag}
|
|
109
|
+
on:keydown={handleSustainKey}
|
|
110
|
+
/>
|
|
111
|
+
<circle
|
|
112
|
+
data-envelope-handle="sustain"
|
|
113
|
+
cx={releaseX}
|
|
114
|
+
cy={sustainY}
|
|
115
|
+
r="4"
|
|
116
|
+
role="button"
|
|
117
|
+
tabindex="0"
|
|
118
|
+
aria-label="Adjust sustain"
|
|
119
|
+
on:pointerdown={beginSustainDrag}
|
|
120
|
+
on:keydown={handleSustainKey}
|
|
121
|
+
/>
|
|
122
|
+
<rect data-envelope-handle="end" x="190" y="84" width="8" height="8" />
|
|
123
|
+
</svg>
|
|
124
|
+
</div>
|
package/src/Gain.svelte
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseComponentProps } from "@patchbayhq/ui";
|
|
3
|
+
import { ratio } from "./shared";
|
|
4
|
+
|
|
5
|
+
export let valueDb = -12;
|
|
6
|
+
export let minDb = -70;
|
|
7
|
+
export let maxDb = 6;
|
|
8
|
+
export let stepDb = 0.1;
|
|
9
|
+
export let level = 0;
|
|
10
|
+
export let thumbSide: "left" | "right" = "right";
|
|
11
|
+
export let disabled = false;
|
|
12
|
+
export let onValueDbChange: ((valueDb: number) => void) | undefined = undefined;
|
|
13
|
+
|
|
14
|
+
let className = "";
|
|
15
|
+
export { className as class };
|
|
16
|
+
|
|
17
|
+
$: props = parseComponentProps("gain", {
|
|
18
|
+
disabled,
|
|
19
|
+
level,
|
|
20
|
+
maxDb,
|
|
21
|
+
minDb,
|
|
22
|
+
stepDb,
|
|
23
|
+
thumbSide,
|
|
24
|
+
valueDb,
|
|
25
|
+
});
|
|
26
|
+
$: gainRatio = ratio(props.valueDb, props.minDb, props.maxDb);
|
|
27
|
+
|
|
28
|
+
function handleInput(event: Event) {
|
|
29
|
+
valueDb = Number((event.currentTarget as HTMLInputElement).value);
|
|
30
|
+
onValueDbChange?.(valueDb);
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<label
|
|
35
|
+
class={["gain", className].filter(Boolean).join(" ")}
|
|
36
|
+
data-gain
|
|
37
|
+
data-thumb-side={props.thumbSide}
|
|
38
|
+
style={`--gain-value: ${gainRatio}; --gain-signal: ${props.level}`}
|
|
39
|
+
>
|
|
40
|
+
<input
|
|
41
|
+
aria-label="Gain"
|
|
42
|
+
type="range"
|
|
43
|
+
min={props.minDb}
|
|
44
|
+
max={props.maxDb}
|
|
45
|
+
step={props.stepDb}
|
|
46
|
+
bind:value={valueDb}
|
|
47
|
+
disabled={props.disabled}
|
|
48
|
+
on:input={handleInput}
|
|
49
|
+
/>
|
|
50
|
+
<span class="gain__signal" data-gain-signal aria-hidden="true"></span>
|
|
51
|
+
<span class="gain__thumb" aria-hidden="true"></span>
|
|
52
|
+
<span class="gain__readout" data-gain-readout>{props.valueDb.toFixed(1)} dB</span>
|
|
53
|
+
</label>
|
package/src/Grid.svelte
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
defineElements,
|
|
4
|
+
parseComponentProps,
|
|
5
|
+
type GridCell,
|
|
6
|
+
type GridChangeDetail,
|
|
7
|
+
type GridDirection,
|
|
8
|
+
type StepColor,
|
|
9
|
+
} from "@patchbayhq/ui";
|
|
10
|
+
import { onMount } from "svelte";
|
|
11
|
+
|
|
12
|
+
export let cells: GridCell[] = Array.from({ length: 16 }, () => ({
|
|
13
|
+
active: false,
|
|
14
|
+
color: "blue" as StepColor,
|
|
15
|
+
y: 0.5,
|
|
16
|
+
}));
|
|
17
|
+
export let directions: GridDirection[] = Array.from({ length: 16 }, () => "right" as GridDirection);
|
|
18
|
+
export let measureSize = 4;
|
|
19
|
+
export let onCellsChange: ((cells: GridCell[]) => void) | undefined = undefined;
|
|
20
|
+
export let onDirectionsChange: ((directions: GridDirection[]) => void) | undefined = undefined;
|
|
21
|
+
|
|
22
|
+
let className = "";
|
|
23
|
+
let element: HTMLElement & {
|
|
24
|
+
cells: GridCell[];
|
|
25
|
+
directions: GridDirection[];
|
|
26
|
+
measureSize: number;
|
|
27
|
+
};
|
|
28
|
+
export { className as class };
|
|
29
|
+
|
|
30
|
+
$: props = parseComponentProps("grid", { cells, directions, measureSize });
|
|
31
|
+
$: if (element) syncElement();
|
|
32
|
+
|
|
33
|
+
function syncElement() {
|
|
34
|
+
element.cells = props.cells;
|
|
35
|
+
element.directions = props.directions;
|
|
36
|
+
element.measureSize = props.measureSize;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleChange(event: Event) {
|
|
40
|
+
const detail = (event as CustomEvent<GridChangeDetail>).detail;
|
|
41
|
+
cells = detail.cells;
|
|
42
|
+
directions = detail.directions;
|
|
43
|
+
onCellsChange?.(cells);
|
|
44
|
+
onDirectionsChange?.(directions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onMount(() => {
|
|
48
|
+
defineElements();
|
|
49
|
+
syncElement();
|
|
50
|
+
element.addEventListener("grid-change", handleChange);
|
|
51
|
+
|
|
52
|
+
return () => element.removeEventListener("grid-change", handleChange);
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<sequencer-grid bind:this={element} class={className}></sequencer-grid>
|
package/src/Label.svelte
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseComponentProps } from "@patchbayhq/ui";
|
|
3
|
+
|
|
4
|
+
export let text = "";
|
|
5
|
+
export let align: "left" | "center" | "right" = "left";
|
|
6
|
+
|
|
7
|
+
let className = "";
|
|
8
|
+
export { className as class };
|
|
9
|
+
|
|
10
|
+
$: props = parseComponentProps("label", { align, text });
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<p class={["label", className].filter(Boolean).join(" ")} style={`text-align: ${props.align}`}>{props.text}</p>
|
package/src/Line.svelte
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseComponentProps, type LineOrientation } from "@patchbayhq/ui";
|
|
3
|
+
|
|
4
|
+
export let orientation: LineOrientation = "horizontal";
|
|
5
|
+
export let length = 170;
|
|
6
|
+
export let thickness = 7;
|
|
7
|
+
|
|
8
|
+
let className = "";
|
|
9
|
+
export { className as class };
|
|
10
|
+
|
|
11
|
+
$: props = parseComponentProps("line", { length, orientation, thickness });
|
|
12
|
+
$: style =
|
|
13
|
+
props.orientation === "vertical"
|
|
14
|
+
? `block-size: ${props.length}px; inline-size: ${props.thickness}px`
|
|
15
|
+
: `block-size: ${props.thickness}px; inline-size: ${props.length}px`;
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<span aria-hidden="true" class={["line", props.orientation === "vertical" ? "line--vertical" : "", className].filter(Boolean).join(" ")} {style}></span>
|