@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 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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>