@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.
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import Dial from "./Dial.svelte";
3
+ import {
4
+ defaultMacroRackMacros,
5
+ type MacroRackMacro,
6
+ } from "./composites";
7
+
8
+ export let columns: 2 | 3 | 4 | 8 = 4;
9
+ export let disabled = false;
10
+ export let label = "Macro rack";
11
+ export let macros: MacroRackMacro[] = defaultMacroRackMacros;
12
+ export let onMacroChange:
13
+ | ((id: string, value: number, macro: MacroRackMacro) => void)
14
+ | undefined = undefined;
15
+
16
+ let className = "";
17
+ export { className as class };
18
+
19
+ function handleMacroChange(macro: MacroRackMacro, value: number) {
20
+ macros = macros.map((item) =>
21
+ item.id === macro.id ? { ...item, value } : item,
22
+ );
23
+ onMacroChange?.(macro.id, value, macro);
24
+ }
25
+ </script>
26
+
27
+ <div
28
+ {...$$restProps}
29
+ aria-label={label}
30
+ class={["macro-rack", className].filter(Boolean).join(" ")}
31
+ role="group"
32
+ style={`--macro-rack-columns: ${columns}`}
33
+ >
34
+ {#each macros as macro (macro.id)}
35
+ <Dial
36
+ class="macro-rack__control"
37
+ disabled={disabled || macro.disabled}
38
+ dragAxis={macro.dragAxis}
39
+ label={macro.label}
40
+ max={macro.max}
41
+ min={macro.min}
42
+ mode={macro.mode}
43
+ onValueChange={(value) => handleMacroChange(macro, value)}
44
+ step={macro.step}
45
+ value={macro.value}
46
+ />
47
+ {/each}
48
+ </div>
@@ -0,0 +1,139 @@
1
+ <script lang="ts">
2
+ import { parseComponentProps, type MenuItem } from "@patchbayhq/ui";
3
+
4
+ export let value = "classic";
5
+ export let items: MenuItem[] = [
6
+ { value: "classic", label: "Classic", disabled: false },
7
+ ];
8
+ export let disabled = false;
9
+ export let onValueChange: ((value: string) => void) | undefined = undefined;
10
+
11
+ const id = `menu-${Math.random().toString(36).slice(2)}`;
12
+ let className = "";
13
+ let open = false;
14
+ let activeIndex = 0;
15
+ export { className as class };
16
+
17
+ $: props = parseComponentProps("menu", { disabled, items, value });
18
+ $: selectedIndex = Math.max(
19
+ 0,
20
+ props.items.findIndex((item) => item.value === props.value),
21
+ );
22
+ $: selected = props.items[selectedIndex] ?? props.items[0];
23
+
24
+ function firstEnabledIndex(startIndex: number, direction: 1 | -1) {
25
+ for (let offset = 0; offset < props.items.length; offset += 1) {
26
+ const index =
27
+ (startIndex + offset * direction + props.items.length) %
28
+ props.items.length;
29
+ if (!props.items[index]?.disabled) return index;
30
+ }
31
+
32
+ return selectedIndex;
33
+ }
34
+
35
+ function commit(index: number) {
36
+ const item = props.items[index];
37
+ if (!item || item.disabled) return;
38
+
39
+ value = item.value;
40
+ activeIndex = index;
41
+ open = false;
42
+ onValueChange?.(value);
43
+ }
44
+
45
+ function toggleOpen() {
46
+ activeIndex = selectedIndex;
47
+ open = !open;
48
+ }
49
+
50
+ function handleKeydown(event: KeyboardEvent) {
51
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
52
+ event.preventDefault();
53
+ const direction = event.key === "ArrowDown" ? 1 : -1;
54
+ activeIndex = firstEnabledIndex(activeIndex + direction, direction);
55
+ open = true;
56
+ return;
57
+ }
58
+
59
+ if (event.key === "Enter" || event.key === " ") {
60
+ event.preventDefault();
61
+ if (open) {
62
+ commit(activeIndex);
63
+ } else {
64
+ open = true;
65
+ }
66
+ return;
67
+ }
68
+
69
+ if (event.key === "Escape") {
70
+ open = false;
71
+ }
72
+ }
73
+
74
+ function handleBlur(event: FocusEvent) {
75
+ const nextTarget = event.relatedTarget;
76
+ if (
77
+ nextTarget instanceof Node &&
78
+ event.currentTarget instanceof HTMLElement &&
79
+ event.currentTarget.contains(nextTarget)
80
+ ) {
81
+ return;
82
+ }
83
+
84
+ open = false;
85
+ }
86
+ </script>
87
+
88
+ <div
89
+ class={["menu", className].filter(Boolean).join(" ")}
90
+ data-open={open ? "true" : undefined}
91
+ on:blur={handleBlur}
92
+ >
93
+ <button
94
+ aria-controls={`${id}-listbox`}
95
+ aria-expanded={open}
96
+ aria-haspopup="listbox"
97
+ class="menu__button"
98
+ {disabled}
99
+ id={`${id}-button`}
100
+ type="button"
101
+ on:click={toggleOpen}
102
+ on:keydown={handleKeydown}
103
+ >
104
+ <span class="menu__value">{selected?.label ?? "Select"}</span>
105
+ <span aria-hidden="true" class="menu__chevron"></span>
106
+ </button>
107
+
108
+ {#if open}
109
+ <div
110
+ aria-activedescendant={`${id}-option-${activeIndex}`}
111
+ class="menu__list"
112
+ id={`${id}-listbox`}
113
+ role="listbox"
114
+ tabindex="-1"
115
+ >
116
+ {#each props.items as item, index (item.value)}
117
+ <button
118
+ aria-disabled={item.disabled ? "true" : undefined}
119
+ aria-selected={item.value === props.value}
120
+ class={[
121
+ "menu__option",
122
+ index === activeIndex && "is-active",
123
+ item.value === props.value && "is-selected",
124
+ ]
125
+ .filter(Boolean)
126
+ .join(" ")}
127
+ disabled={item.disabled}
128
+ id={`${id}-option-${index}`}
129
+ role="option"
130
+ type="button"
131
+ on:click={() => commit(index)}
132
+ on:mouseenter={() => (activeIndex = index)}
133
+ >
134
+ {item.label}
135
+ </button>
136
+ {/each}
137
+ </div>
138
+ {/if}
139
+ </div>
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { parseComponentProps, type Orientation } from "@patchbayhq/ui";
3
+
4
+ export let level = 0;
5
+ export let peak: number | undefined = undefined;
6
+ export let orientation: Orientation = "vertical";
7
+
8
+ let className = "";
9
+ export { className as class };
10
+
11
+ $: props = parseComponentProps("meter", { level, orientation, peak });
12
+ </script>
13
+
14
+ <div aria-label="Signal meter" class={["meter", className].filter(Boolean).join(" ")} data-meter>
15
+ <span
16
+ aria-hidden="true"
17
+ class="meter__bar"
18
+ data-meter-bar
19
+ style={`--meter-value: ${props.level}`}
20
+ ></span>
21
+ </div>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { parseComponentProps, type NumberFormat } from "@patchbayhq/ui";
3
+
4
+ export let label = "Number";
5
+ export let value = 0;
6
+ export let min = 0;
7
+ export let max = 127;
8
+ export let step = 1;
9
+ export let format: NumberFormat = "integer";
10
+ export let disabled = false;
11
+ export let onValueChange: ((value: number) => void) | undefined = undefined;
12
+
13
+ let className = "";
14
+ export { className as class };
15
+
16
+ $: props = parseComponentProps("number-box", { disabled, format, label, max, min, step, value });
17
+
18
+ function handleInput(event: Event) {
19
+ value = Number((event.currentTarget as HTMLInputElement).value);
20
+ onValueChange?.(value);
21
+ }
22
+ </script>
23
+
24
+ <input
25
+ aria-label={props.label}
26
+ class={["number-box", className].filter(Boolean).join(" ")}
27
+ disabled={props.disabled}
28
+ max={props.max}
29
+ min={props.min}
30
+ step={props.step}
31
+ type="number"
32
+ bind:value
33
+ on:input={handleInput}
34
+ />
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ export let compact = false;
3
+
4
+ let className = "";
5
+ export { className as class };
6
+ </script>
7
+
8
+ <section
9
+ {...$$restProps}
10
+ class={["panel", compact && "panel--compact", className].filter(Boolean).join(" ")}
11
+ >
12
+ <slot />
13
+ </section>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ export let eyebrow: string | undefined = undefined;
3
+ export let subtitle: string | undefined = undefined;
4
+ export let title: string | undefined = undefined;
5
+
6
+ let className = "";
7
+ export { className as class };
8
+ </script>
9
+
10
+ <header
11
+ {...$$restProps}
12
+ class={["panel-header", className].filter(Boolean).join(" ")}
13
+ >
14
+ <div class="panel-header__main">
15
+ {#if eyebrow}
16
+ <span class="panel-header__eyebrow">{eyebrow}</span>
17
+ {/if}
18
+ {#if title}
19
+ <h3 class="panel-header__title">{title}</h3>
20
+ {/if}
21
+ {#if subtitle}
22
+ <p class="panel-header__subtitle">{subtitle}</p>
23
+ {/if}
24
+ </div>
25
+ <div class="panel-header__actions">
26
+ <slot />
27
+ </div>
28
+ </header>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ export let title: string | undefined = undefined;
3
+
4
+ let className = "";
5
+ export { className as class };
6
+ </script>
7
+
8
+ <section
9
+ {...$$restProps}
10
+ class={["panel-section", className].filter(Boolean).join(" ")}
11
+ >
12
+ {#if title}
13
+ <div class="panel-section__header">
14
+ <h4 class="panel-section__title">{title}</h4>
15
+ <div class="panel-section__actions">
16
+ <slot name="actions" />
17
+ </div>
18
+ </div>
19
+ {/if}
20
+ <div class="panel-section__body">
21
+ <slot />
22
+ </div>
23
+ </section>
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { parseComponentProps } from "@patchbayhq/ui";
4
+
5
+ export let samples: number[] = [];
6
+ export let mode: "waveform" | "lissajous" = "waveform";
7
+ export let frozen = false;
8
+
9
+ let className = "";
10
+ let canvas: HTMLCanvasElement;
11
+ export { className as class };
12
+
13
+ $: props = parseComponentProps("scope", { frozen, mode, samples });
14
+ $: if (canvas && props) draw();
15
+
16
+ function draw() {
17
+ const context = canvas.getContext("2d");
18
+ if (!context) return;
19
+
20
+ context.clearRect(0, 0, canvas.width, canvas.height);
21
+ context.fillStyle = "#161616";
22
+ context.fillRect(0, 0, canvas.width, canvas.height);
23
+ context.strokeStyle = "rgba(255, 255, 255, 0.12)";
24
+ context.lineWidth = 1;
25
+
26
+ for (let x = 0; x < canvas.width; x += 23) {
27
+ context.beginPath();
28
+ context.moveTo(x, 0);
29
+ context.lineTo(x, canvas.height);
30
+ context.stroke();
31
+ }
32
+
33
+ const wave = props.samples.length > 0 ? props.samples : Array.from({ length: canvas.width }, () => 0);
34
+ context.strokeStyle = "#9cd8ca";
35
+ context.lineWidth = 2;
36
+ context.beginPath();
37
+
38
+ wave.forEach((sample, index) => {
39
+ const x = (index / Math.max(1, wave.length - 1)) * canvas.width;
40
+ const y = canvas.height / 2 - Math.max(-1, Math.min(1, sample)) * (canvas.height * 0.38);
41
+ index === 0 ? context.moveTo(x, y) : context.lineTo(x, y);
42
+ });
43
+
44
+ context.stroke();
45
+ }
46
+
47
+ onMount(draw);
48
+ </script>
49
+
50
+ <canvas bind:this={canvas} aria-label="Scope" class={["scope", className].filter(Boolean).join(" ")} data-scope width="184" height="98"></canvas>
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import { parseComponentProps, type Orientation } from "@patchbayhq/ui";
3
+ import { init } from "./shared";
4
+
5
+ export let label = "Slider";
6
+ export let value = 0;
7
+ export let min = 0;
8
+ export let max = 1;
9
+ export let step: number | "any" = 0.01;
10
+ export let orientation: Orientation = "vertical";
11
+ export let modulation: number | undefined = undefined;
12
+ export let disabled = false;
13
+ export let onValueChange: ((value: number) => void) | undefined = undefined;
14
+
15
+ let className = "";
16
+ export { className as class };
17
+
18
+ $: props = parseComponentProps("slider", {
19
+ disabled,
20
+ label,
21
+ max,
22
+ min,
23
+ modulation,
24
+ orientation,
25
+ step,
26
+ value,
27
+ });
28
+
29
+ function handleInput(event: Event) {
30
+ value = Number((event.currentTarget as HTMLInputElement).value);
31
+ onValueChange?.(value);
32
+ }
33
+ </script>
34
+
35
+ <label class={["field", className].filter(Boolean).join(" ")}>
36
+ <span class="field__label">{props.label}</span>
37
+ <span
38
+ class="slider"
39
+ data-slider
40
+ data-orientation={props.orientation}
41
+ data-modulation={props.modulation}
42
+ use:init={[props.value, props.modulation, props.orientation]}
43
+ >
44
+ <input
45
+ aria-label={props.label}
46
+ type="range"
47
+ min={props.min}
48
+ max={props.max}
49
+ step={props.step}
50
+ bind:value
51
+ {disabled}
52
+ on:input={handleInput}
53
+ />
54
+ </span>
55
+ </label>
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import type { StatusTone } from "./composites";
3
+
4
+ export let label = "";
5
+ export let pulse = false;
6
+ export let tone: StatusTone = "idle";
7
+ export let value = "";
8
+
9
+ let className = "";
10
+ export { className as class };
11
+ </script>
12
+
13
+ <span
14
+ {...$$restProps}
15
+ class={["status-indicator", className].filter(Boolean).join(" ")}
16
+ data-pulse={pulse ? "true" : undefined}
17
+ data-tone={tone}
18
+ >
19
+ <span aria-hidden="true" class="status-indicator__dot"></span>
20
+ {#if label}
21
+ <span class="status-indicator__label">{label}</span>
22
+ {/if}
23
+ {#if value}
24
+ <strong class="status-indicator__value">{value}</strong>
25
+ {/if}
26
+ </span>
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import {
3
+ defineElements,
4
+ parseComponentProps,
5
+ type StepChangeDetail,
6
+ type StepKey,
7
+ type StepLoop,
8
+ type StepNote,
9
+ } from "@patchbayhq/ui";
10
+ import { onMount } from "svelte";
11
+
12
+ export let activeKey: StepKey = "C4";
13
+ export let notes: StepNote[] = [];
14
+ export let loop: StepLoop = { start: 1, end: 16 };
15
+ export let bars = 4;
16
+ export let onChange: ((detail: StepChangeDetail) => void) | undefined = undefined;
17
+ export let onCellChange:
18
+ | ((change: Extract<StepChangeDetail, { type: "cell" }>) => void)
19
+ | undefined = undefined;
20
+ export let onKeyChange:
21
+ | ((change: Extract<StepChangeDetail, { type: "key" }>) => void)
22
+ | undefined = undefined;
23
+ export let onKeyPress: ((key: StepKey) => void) | undefined = undefined;
24
+ export let onKeyRelease: ((key: StepKey) => void) | undefined = undefined;
25
+ export let onLoopChange: ((loop: StepLoop) => void) | undefined = undefined;
26
+ export let onNotesChange: ((notes: StepNote[]) => void) | undefined = undefined;
27
+
28
+ let className = "";
29
+ let element: HTMLElement & {
30
+ activeKey: StepKey;
31
+ bars: number;
32
+ loop: StepLoop;
33
+ notes: StepNote[];
34
+ };
35
+ export { className as class };
36
+
37
+ $: props = parseComponentProps("step-sequencer", { activeKey, bars, loop, notes });
38
+ $: if (element) syncElement();
39
+
40
+ function syncElement() {
41
+ element.activeKey = props.activeKey;
42
+ element.bars = props.bars;
43
+ element.loop = props.loop;
44
+ element.notes = props.notes;
45
+ }
46
+
47
+ function handleChange(event: Event) {
48
+ const detail = (event as CustomEvent<StepChangeDetail>).detail;
49
+ activeKey = detail.activeKey;
50
+ loop = detail.loop;
51
+ notes = detail.notes;
52
+ onChange?.(detail);
53
+ if (detail.type === "key") {
54
+ onKeyChange?.(detail);
55
+ if (detail.pressed) {
56
+ onKeyPress?.(detail.key);
57
+ } else {
58
+ onKeyRelease?.(detail.key);
59
+ }
60
+ }
61
+ if (detail.type === "loop") onLoopChange?.(detail.loop);
62
+ if (detail.type === "cell") {
63
+ onCellChange?.(detail);
64
+ onNotesChange?.(notes);
65
+ }
66
+ }
67
+
68
+ onMount(() => {
69
+ defineElements();
70
+ syncElement();
71
+ element.addEventListener("step-change", handleChange);
72
+
73
+ return () => element.removeEventListener("step-change", handleChange);
74
+ });
75
+ </script>
76
+
77
+ <step-sequencer bind:this={element} class={className}></step-sequencer>
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import { parseComponentProps, type TabItem } from "@patchbayhq/ui";
3
+
4
+ export let value = "one";
5
+ export let items: TabItem[] = [{ value: "one", label: "One", disabled: false }];
6
+ export let disabled = false;
7
+ export let onValueChange: ((value: string) => void) | undefined = undefined;
8
+
9
+ let className = "";
10
+ export { className as class };
11
+
12
+ $: props = parseComponentProps("tabs", { disabled, items, value });
13
+
14
+ function select(nextValue: string) {
15
+ value = nextValue;
16
+ onValueChange?.(value);
17
+ }
18
+ </script>
19
+
20
+ <div class={["tabs", className].filter(Boolean).join(" ")} role="tablist">
21
+ {#each props.items as item (item.value)}
22
+ <button
23
+ aria-selected={item.value === props.value}
24
+ class={["tab", item.value === props.value ? "is-active" : ""].filter(Boolean).join(" ")}
25
+ disabled={props.disabled || item.disabled}
26
+ role="tab"
27
+ type="button"
28
+ on:click={() => select(item.value)}
29
+ >
30
+ {item.label}
31
+ </button>
32
+ {/each}
33
+ </div>
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import {
3
+ parseComponentProps,
4
+ type ControlAppearance,
5
+ } from "@patchbayhq/ui";
6
+
7
+ export let value = false;
8
+ export let labels = { off: "Off", on: "On" };
9
+ export let icon: string | undefined = undefined;
10
+ export let picture: string | undefined = undefined;
11
+ export let pictureAlt = "";
12
+ export let appearance: ControlAppearance = "default";
13
+ export let disabled = false;
14
+ export let onValueChange: ((value: boolean) => void) | undefined = undefined;
15
+
16
+ let className = "";
17
+ export { className as class };
18
+
19
+ $: props = parseComponentProps("text-button", {
20
+ appearance,
21
+ disabled,
22
+ icon,
23
+ labels,
24
+ picture,
25
+ pictureAlt,
26
+ value,
27
+ });
28
+ $: label = props.value ? props.labels.on : props.labels.off;
29
+
30
+ function toggle() {
31
+ value = !props.value;
32
+ onValueChange?.(value);
33
+ }
34
+ </script>
35
+
36
+ <button
37
+ aria-pressed={props.value}
38
+ class={["text-button", className].filter(Boolean).join(" ")}
39
+ data-appearance={props.appearance}
40
+ {disabled}
41
+ type="button"
42
+ on:click={toggle}
43
+ >
44
+ {#if props.picture}
45
+ <img
46
+ alt={props.pictureAlt}
47
+ class="text-button__picture"
48
+ src={props.picture}
49
+ />
50
+ {:else if props.icon}
51
+ <span aria-hidden="true" class="text-button__icon">{props.icon}</span>
52
+ {/if}
53
+ <span class="text-button__label"><slot>{label}</slot></span>
54
+ </button>
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { parseComponentProps, type ControlAppearance } from "@patchbayhq/ui";
3
+
4
+ export let label = "Toggle";
5
+ export let checked = false;
6
+ export let appearance: ControlAppearance = "default";
7
+ export let disabled = false;
8
+ export let onCheckedChange: ((checked: boolean) => void) | undefined = undefined;
9
+
10
+ let className = "";
11
+ export { className as class };
12
+
13
+ $: props = parseComponentProps("toggle", {
14
+ appearance,
15
+ checked,
16
+ disabled,
17
+ label,
18
+ });
19
+
20
+ function handleChange(event: Event) {
21
+ checked = (event.currentTarget as HTMLInputElement).checked;
22
+ onCheckedChange?.(checked);
23
+ }
24
+ </script>
25
+
26
+ <label
27
+ class={["toggle", className].filter(Boolean).join(" ")}
28
+ data-appearance={props.appearance}
29
+ >
30
+ <input aria-label={props.label} type="checkbox" bind:checked {disabled} on:change={handleChange} />
31
+ <span aria-hidden="true"></span>
32
+ </label>
@@ -0,0 +1,35 @@
1
+ import type {
2
+ DialProps,
3
+ StepChangeDetail,
4
+ StepKey,
5
+ StepLoop,
6
+ StepNote,
7
+ } from "@patchbayhq/ui";
8
+
9
+ export type StatusTone = "idle" | "active" | "warning" | "danger";
10
+
11
+ export type MacroRackMacro = {
12
+ dragAxis?: DialProps["dragAxis"];
13
+ disabled?: boolean;
14
+ id: string;
15
+ label: string;
16
+ max?: number;
17
+ min?: number;
18
+ mode?: DialProps["mode"];
19
+ step?: DialProps["step"];
20
+ value: number;
21
+ };
22
+
23
+ export const defaultMacroRackMacros: MacroRackMacro[] = Array.from(
24
+ { length: 8 },
25
+ (_, index) => ({
26
+ id: `macro-${index + 1}`,
27
+ label: `Macro ${index + 1}`,
28
+ value: 0,
29
+ }),
30
+ );
31
+
32
+ export type ClipStepLauncherChange = StepChangeDetail;
33
+ export type ClipStepLauncherKey = StepKey;
34
+ export type ClipStepLauncherLoop = StepLoop;
35
+ export type ClipStepLauncherNote = StepNote;