@ctrl-ui/react 0.9.0 → 0.9.2
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/audio-device-select.d.ts +3 -1
- package/dist/audio-device-select.js +35 -13
- package/dist/knob-contracts/channel-strip-knobs.d.ts +4 -0
- package/dist/knob-contracts/channel-strip-knobs.js +9 -0
- package/dist/knob-contracts/range-knobs.d.ts +1 -1
- package/dist/knob-contracts/range-knobs.js +4 -0
- package/dist/lib/level-meter-ballistics.d.ts +17 -0
- package/dist/lib/level-meter-ballistics.js +32 -0
- package/dist/styles/index.css +3 -1
- package/dist/styles/recipes/channel-strip.css +75 -0
- package/dist/styles/recipes/range-level-meter.css +158 -0
- package/dist/ui/channel-strip.d.ts +26 -0
- package/dist/ui/channel-strip.js +89 -0
- package/dist/ui/level-meter.d.ts +36 -0
- package/dist/ui/level-meter.js +201 -0
- package/package.json +1 -1
|
@@ -10,8 +10,10 @@ export type AudioDevice = {
|
|
|
10
10
|
status?: "available" | "unavailable" | "permission-required";
|
|
11
11
|
};
|
|
12
12
|
export type AudioDevicePermission = "granted" | "prompt" | "denied";
|
|
13
|
+
export type AudioDeviceKind = "input" | "output";
|
|
13
14
|
export type AudioDeviceSelectProps = Omit<SelectProps, "items" | "children"> & {
|
|
14
15
|
devices: readonly AudioDevice[];
|
|
16
|
+
kind?: AudioDeviceKind;
|
|
15
17
|
loading?: boolean;
|
|
16
18
|
permission?: AudioDevicePermission;
|
|
17
19
|
onRequestPermission?: () => void | Promise<void>;
|
|
@@ -22,7 +24,7 @@ export type AudioDeviceSelectProps = Omit<SelectProps, "items" | "children"> & {
|
|
|
22
24
|
className?: string;
|
|
23
25
|
children?: ReactNode;
|
|
24
26
|
};
|
|
25
|
-
export declare function AudioDeviceSelect({ devices, value, defaultValue, onValueChange, disabled, loading, permission, onRequestPermission, allowNone, noneLabel, label, error, className, children, ...props }: AudioDeviceSelectProps): import("react").JSX.Element;
|
|
27
|
+
export declare function AudioDeviceSelect({ devices, kind, value, defaultValue, onValueChange, disabled, loading, permission, onRequestPermission, allowNone, noneLabel, label, error, className, children, ...props }: AudioDeviceSelectProps): import("react").JSX.Element;
|
|
26
28
|
export declare function AudioDeviceSelectTrigger({ children, className, ...props }: SelectTriggerProps): import("react").JSX.Element;
|
|
27
29
|
export declare function AudioDeviceSelectValue({ placeholder }: {
|
|
28
30
|
placeholder?: string;
|
|
@@ -5,18 +5,36 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select.js
|
|
|
5
5
|
import { Text } from "./ui/typography.js";
|
|
6
6
|
import { createContext, use, useState } from "react";
|
|
7
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { MicIcon } from "lucide-react";
|
|
8
|
+
import { MicIcon, Volume2Icon } from "lucide-react";
|
|
9
9
|
//#region src/audio-device-select.tsx
|
|
10
|
+
const AUDIO_DEVICE_COPY = {
|
|
11
|
+
input: {
|
|
12
|
+
label: "Microphone",
|
|
13
|
+
fallbackName: "Microphone",
|
|
14
|
+
placeholder: "Select a microphone",
|
|
15
|
+
noneLabel: "No microphone",
|
|
16
|
+
empty: "No microphones found",
|
|
17
|
+
disconnected: "This microphone is disconnected. Reconnect it or choose another device."
|
|
18
|
+
},
|
|
19
|
+
output: {
|
|
20
|
+
label: "Output",
|
|
21
|
+
fallbackName: "Speaker",
|
|
22
|
+
placeholder: "Select an output",
|
|
23
|
+
noneLabel: "No output",
|
|
24
|
+
empty: "No outputs found",
|
|
25
|
+
disconnected: "This output is disconnected. Reconnect it or choose another device."
|
|
26
|
+
}
|
|
27
|
+
};
|
|
10
28
|
const AudioDeviceSelectContext = createContext(null);
|
|
11
29
|
function useAudioDeviceSelect() {
|
|
12
30
|
const context = use(AudioDeviceSelectContext);
|
|
13
31
|
if (!context) throw new Error("AudioDeviceSelect parts must be rendered inside AudioDeviceSelect.");
|
|
14
32
|
return context;
|
|
15
33
|
}
|
|
16
|
-
function deviceLabel(device, index = 0) {
|
|
17
|
-
return device.label.trim() ||
|
|
34
|
+
function deviceLabel(device, kind, index = 0) {
|
|
35
|
+
return device.label.trim() || `${AUDIO_DEVICE_COPY[kind].fallbackName} ${index + 1}`;
|
|
18
36
|
}
|
|
19
|
-
function AudioDeviceSelect({ devices, value, defaultValue, onValueChange, disabled = false, loading = false, permission = "granted", onRequestPermission, allowNone = false, noneLabel =
|
|
37
|
+
function AudioDeviceSelect({ devices, kind = "input", value, defaultValue, onValueChange, disabled = false, loading = false, permission = "granted", onRequestPermission, allowNone = false, noneLabel = AUDIO_DEVICE_COPY[kind].noneLabel, label = AUDIO_DEVICE_COPY[kind].label, error = null, className, children, ...props }) {
|
|
20
38
|
const [internalValue, setInternalValue] = useState(() => defaultValue ?? null);
|
|
21
39
|
const current = value === void 0 ? internalValue : value;
|
|
22
40
|
const connected = devices.find((device) => device.deviceId === current) ?? null;
|
|
@@ -30,6 +48,7 @@ function AudioDeviceSelect({ devices, value, defaultValue, onValueChange, disabl
|
|
|
30
48
|
}
|
|
31
49
|
return /* @__PURE__ */ jsx(AudioDeviceSelectContext.Provider, {
|
|
32
50
|
value: {
|
|
51
|
+
kind,
|
|
33
52
|
devices,
|
|
34
53
|
selected,
|
|
35
54
|
value: current,
|
|
@@ -75,18 +94,21 @@ function AudioDeviceSelectTrigger({ children, className, ...props }) {
|
|
|
75
94
|
"aria-busy": context.loading || void 0,
|
|
76
95
|
className: cn("w-full", className),
|
|
77
96
|
...props,
|
|
78
|
-
children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(
|
|
97
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [context.kind === "output" ? /* @__PURE__ */ jsx(Volume2Icon, {
|
|
98
|
+
"aria-hidden": "true",
|
|
99
|
+
className: "size-4 shrink-0"
|
|
100
|
+
}) : /* @__PURE__ */ jsx(MicIcon, {
|
|
79
101
|
"aria-hidden": "true",
|
|
80
102
|
className: "size-4 shrink-0"
|
|
81
103
|
}), /* @__PURE__ */ jsx(AudioDeviceSelectValue, {})] })
|
|
82
104
|
});
|
|
83
105
|
}
|
|
84
|
-
function AudioDeviceSelectValue({ placeholder
|
|
106
|
+
function AudioDeviceSelectValue({ placeholder }) {
|
|
85
107
|
const context = useAudioDeviceSelect();
|
|
86
|
-
let text = placeholder;
|
|
87
|
-
if (context.selected) text = deviceLabel(context.selected, context.devices.indexOf(context.selected));
|
|
108
|
+
let text = placeholder ?? AUDIO_DEVICE_COPY[context.kind].placeholder;
|
|
109
|
+
if (context.selected) text = deviceLabel(context.selected, context.kind, context.devices.indexOf(context.selected));
|
|
88
110
|
if (context.allowNone && context.value === "") text = context.noneLabel;
|
|
89
|
-
if (context.missing) text = `${context.selected ? deviceLabel(context.selected) : "Unknown device"} (disconnected)`;
|
|
111
|
+
if (context.missing) text = `${context.selected ? deviceLabel(context.selected, context.kind) : "Unknown device"} (disconnected)`;
|
|
90
112
|
if (context.loading) text = "Finding devices…";
|
|
91
113
|
return /* @__PURE__ */ jsx("span", {
|
|
92
114
|
"data-control-ui": "audio-device-select",
|
|
@@ -114,11 +136,11 @@ function AudioDeviceSelectContent({ children, ...props }) {
|
|
|
114
136
|
}) : null,
|
|
115
137
|
context.devices.filter((device) => !context.allowNone || device.deviceId !== "").map((device, index) => /* @__PURE__ */ jsxs(AudioDeviceSelectItem, {
|
|
116
138
|
value: device.deviceId,
|
|
117
|
-
label: deviceLabel(device, index),
|
|
139
|
+
label: deviceLabel(device, context.kind, index),
|
|
118
140
|
disabled: device.status === "unavailable" || device.status === "permission-required",
|
|
119
141
|
children: [/* @__PURE__ */ jsx("span", {
|
|
120
142
|
className: "min-w-0 flex-1 truncate",
|
|
121
|
-
children: deviceLabel(device, index)
|
|
143
|
+
children: deviceLabel(device, context.kind, index)
|
|
122
144
|
}), device.isDefault || device.deviceId === "default" ? /* @__PURE__ */ jsx(Text, {
|
|
123
145
|
size: "caption",
|
|
124
146
|
tone: "muted",
|
|
@@ -129,7 +151,7 @@ function AudioDeviceSelectContent({ children, ...props }) {
|
|
|
129
151
|
context.devices.length === 0 && !context.missing && !context.allowNone ? /* @__PURE__ */ jsx(SelectItem, {
|
|
130
152
|
value: "__audio-device-empty__",
|
|
131
153
|
disabled: true,
|
|
132
|
-
children: context.permission === "granted" ?
|
|
154
|
+
children: context.permission === "granted" ? AUDIO_DEVICE_COPY[context.kind].empty : "Allow microphone access to find devices"
|
|
133
155
|
}) : null
|
|
134
156
|
] })
|
|
135
157
|
});
|
|
@@ -153,7 +175,7 @@ function AudioDeviceSelectStatus({ className, ...props }) {
|
|
|
153
175
|
const context = useAudioDeviceSelect();
|
|
154
176
|
let message = context.error;
|
|
155
177
|
if (!message && context.permission === "denied") message = "Microphone access is blocked. Allow it in your browser’s site settings.";
|
|
156
|
-
if (!message && context.missing) message =
|
|
178
|
+
if (!message && context.missing) message = AUDIO_DEVICE_COPY[context.kind].disconnected;
|
|
157
179
|
if (!message) return null;
|
|
158
180
|
return /* @__PURE__ */ jsx("p", {
|
|
159
181
|
"data-control-ui": "audio-device-select",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/knob-contracts/channel-strip-knobs.d.ts
|
|
2
|
+
export declare const channelStripKnobs: readonly ["--cui-channel-strip-size", "--cui-channel-strip-body-height", "--cui-channel-strip-separator-color", "--cui-channel-strip-value-foreground"];
|
|
3
|
+
export type ChannelStripKnobStyle = Partial<Record<(typeof channelStripKnobs)[number], string>>;
|
|
4
|
+
//#endregion
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/knob-contracts/channel-strip-knobs.ts
|
|
2
|
+
const channelStripKnobs = [
|
|
3
|
+
"--cui-channel-strip-size",
|
|
4
|
+
"--cui-channel-strip-body-height",
|
|
5
|
+
"--cui-channel-strip-separator-color",
|
|
6
|
+
"--cui-channel-strip-value-foreground"
|
|
7
|
+
];
|
|
8
|
+
//#endregion
|
|
9
|
+
export { channelStripKnobs };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
//#region src/knob-contracts/range-knobs.d.ts
|
|
2
|
-
export declare const rangeKnobs: readonly ["--cui-range-indeterminate-size", "--cui-range-track-radius", "--cui-range-track-background", "--cui-range-indicator-radius", "--cui-range-indicator-background", "--cui-range-thumb-radius", "--cui-range-thumb-background", "--cui-range-thumb-border-color", "--cui-range-thumb-shadow"];
|
|
2
|
+
export declare const rangeKnobs: readonly ["--cui-range-level-warning-background", "--cui-range-level-clip-background", "--cui-range-level-size", "--cui-range-level-scale-foreground", "--cui-range-indeterminate-size", "--cui-range-track-radius", "--cui-range-track-background", "--cui-range-indicator-radius", "--cui-range-indicator-background", "--cui-range-thumb-radius", "--cui-range-thumb-background", "--cui-range-thumb-border-color", "--cui-range-thumb-shadow"];
|
|
3
3
|
export type RangeKnobStyle = Partial<Record<(typeof rangeKnobs)[number], string>>;
|
|
4
4
|
//#endregion
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
//#region src/knob-contracts/range-knobs.ts
|
|
2
2
|
const rangeKnobs = [
|
|
3
|
+
"--cui-range-level-warning-background",
|
|
4
|
+
"--cui-range-level-clip-background",
|
|
5
|
+
"--cui-range-level-size",
|
|
6
|
+
"--cui-range-level-scale-foreground",
|
|
3
7
|
"--cui-range-indeterminate-size",
|
|
4
8
|
"--cui-range-track-radius",
|
|
5
9
|
"--cui-range-track-background",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/lib/level-meter-ballistics.d.ts
|
|
2
|
+
export declare const LEVEL_METER_RELEASE_DB_PER_SECOND = 24;
|
|
3
|
+
export declare const LEVEL_METER_PEAK_HOLD_MS = 1000;
|
|
4
|
+
export type LevelMeterRange = {
|
|
5
|
+
minDb: number;
|
|
6
|
+
maxDb: number;
|
|
7
|
+
};
|
|
8
|
+
export type LevelMeterReading = {
|
|
9
|
+
levelDb: number;
|
|
10
|
+
peakDb: number;
|
|
11
|
+
peakHeldUntilMs: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function clampLevelDb(levelDb: number, { minDb, maxDb }: LevelMeterRange): number;
|
|
14
|
+
export declare function levelMeterFraction(levelDb: number, scale: LevelMeterRange): number;
|
|
15
|
+
export declare function advanceLevelMeterReading(reading: LevelMeterReading, targetDb: number, elapsedMs: number, nowMs: number, scale: LevelMeterRange): LevelMeterReading;
|
|
16
|
+
export declare function levelMeterReadingSettled(reading: LevelMeterReading, targetDb: number, scale: LevelMeterRange): boolean;
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/lib/level-meter-ballistics.ts
|
|
2
|
+
const LEVEL_METER_RELEASE_DB_PER_SECOND = 24;
|
|
3
|
+
const LEVEL_METER_PEAK_HOLD_MS = 1e3;
|
|
4
|
+
function clampLevelDb(levelDb, { minDb, maxDb }) {
|
|
5
|
+
if (Number.isNaN(levelDb)) return minDb;
|
|
6
|
+
return Math.min(maxDb, Math.max(minDb, levelDb));
|
|
7
|
+
}
|
|
8
|
+
function levelMeterFraction(levelDb, scale) {
|
|
9
|
+
const span = scale.maxDb - scale.minDb;
|
|
10
|
+
if (!(span > 0)) return 0;
|
|
11
|
+
return (clampLevelDb(levelDb, scale) - scale.minDb) / span;
|
|
12
|
+
}
|
|
13
|
+
function advanceLevelMeterReading(reading, targetDb, elapsedMs, nowMs, scale) {
|
|
14
|
+
const target = clampLevelDb(targetDb, scale);
|
|
15
|
+
const fallDb = 24 * Math.max(0, elapsedMs) / 1e3;
|
|
16
|
+
const levelDb = Math.max(target, reading.levelDb - fallDb);
|
|
17
|
+
if (levelDb >= reading.peakDb) return {
|
|
18
|
+
levelDb,
|
|
19
|
+
peakDb: levelDb,
|
|
20
|
+
peakHeldUntilMs: nowMs + LEVEL_METER_PEAK_HOLD_MS
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
levelDb,
|
|
24
|
+
peakDb: nowMs < reading.peakHeldUntilMs ? reading.peakDb : Math.max(levelDb, reading.peakDb - fallDb),
|
|
25
|
+
peakHeldUntilMs: reading.peakHeldUntilMs
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function levelMeterReadingSettled(reading, targetDb, scale) {
|
|
29
|
+
return reading.levelDb === clampLevelDb(targetDb, scale) && reading.peakDb === reading.levelDb;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { LEVEL_METER_PEAK_HOLD_MS, LEVEL_METER_RELEASE_DB_PER_SECOND, advanceLevelMeterReading, clampLevelDb, levelMeterFraction, levelMeterReadingSettled };
|
package/dist/styles/index.css
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
@import "./recipes/button-group.css";
|
|
34
34
|
@import "./recipes/calendar.css";
|
|
35
35
|
@import "./recipes/card.css";
|
|
36
|
+
@import "./recipes/channel-strip.css";
|
|
36
37
|
@import "./recipes/chart.css";
|
|
37
38
|
@import "./recipes/chat-message.css";
|
|
38
39
|
@import "./recipes/chat-message-edit.css";
|
|
@@ -75,8 +76,9 @@
|
|
|
75
76
|
@import "./recipes/inline-attachment.css";
|
|
76
77
|
@import "./recipes/source-badge.css";
|
|
77
78
|
@import "./recipes/inline-citation.css";
|
|
78
|
-
@import "./recipes/markdown-block.css";
|
|
79
79
|
@import "./recipes/range.css";
|
|
80
|
+
@import "./recipes/range-level-meter.css";
|
|
81
|
+
@import "./recipes/markdown-block.css";
|
|
80
82
|
@import "./recipes/morphing-panel.css";
|
|
81
83
|
@import "./recipes/pagination.css";
|
|
82
84
|
@import "./recipes/phone-input.css";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
:where([data-control-family="channel-strip"][data-slot="mixer"]) {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-auto-columns: max-content;
|
|
5
|
+
grid-auto-flow: column;
|
|
6
|
+
grid-template-rows: repeat(4, auto);
|
|
7
|
+
gap: calc(var(--spacing) * 2);
|
|
8
|
+
overflow-x: auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:where([data-control-family="channel-strip"][data-slot="root"]) {
|
|
12
|
+
--cui-channel-strip-size: calc(var(--spacing) * 24);
|
|
13
|
+
--cui-channel-strip-body-height: calc(var(--spacing) * 40);
|
|
14
|
+
--cui-channel-strip-separator-color: var(--border);
|
|
15
|
+
--cui-channel-strip-value-foreground: var(--muted-foreground);
|
|
16
|
+
inline-size: var(--cui-channel-strip-size);
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-row: span 4;
|
|
19
|
+
grid-template-rows: subgrid;
|
|
20
|
+
row-gap: calc(var(--spacing) * 2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:where([data-control-family="channel-strip"][data-slot="root"][data-variant="master"]) {
|
|
24
|
+
border-inline-start: 1px solid var(--cui-channel-strip-separator-color);
|
|
25
|
+
padding-inline-start: calc(var(--spacing) * 2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:where([data-control-family="channel-strip"][data-slot="header"]) {
|
|
29
|
+
grid-row: 1;
|
|
30
|
+
gap: var(--spacing);
|
|
31
|
+
min-block-size: calc(var(--spacing) * 6);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:where([data-control-family="channel-strip"][data-slot="body"]) {
|
|
35
|
+
grid-row: 2;
|
|
36
|
+
gap: calc(var(--spacing) * 2);
|
|
37
|
+
block-size: var(--cui-channel-strip-body-height);
|
|
38
|
+
padding-block: calc(var(--spacing) * 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:where([data-control-family="channel-strip"][data-slot="value"]) {
|
|
42
|
+
grid-row: 3;
|
|
43
|
+
color: var(--cui-channel-strip-value-foreground);
|
|
44
|
+
text-align: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:where([data-control-family="channel-strip"][data-slot="controls"]) {
|
|
48
|
+
grid-row: 4;
|
|
49
|
+
gap: var(--spacing);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@property --cui-channel-strip-size {
|
|
54
|
+
syntax: "<length-percentage>";
|
|
55
|
+
inherits: true;
|
|
56
|
+
initial-value: 0px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@property --cui-channel-strip-body-height {
|
|
60
|
+
syntax: "<length-percentage>";
|
|
61
|
+
inherits: true;
|
|
62
|
+
initial-value: 0px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@property --cui-channel-strip-separator-color {
|
|
66
|
+
syntax: "<color>";
|
|
67
|
+
inherits: true;
|
|
68
|
+
initial-value: transparent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@property --cui-channel-strip-value-foreground {
|
|
72
|
+
syntax: "<color>";
|
|
73
|
+
inherits: true;
|
|
74
|
+
initial-value: transparent;
|
|
75
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="root"]) {
|
|
3
|
+
--cui-range-level-warning-background: var(--warning);
|
|
4
|
+
--cui-range-level-clip-background: var(--destructive);
|
|
5
|
+
--cui-range-level-size: calc(var(--spacing) * 2);
|
|
6
|
+
--cui-range-level-scale-foreground: var(--muted-foreground);
|
|
7
|
+
--_range-level-peak-size: 2px;
|
|
8
|
+
--_range-level-zone-angle: 90deg;
|
|
9
|
+
gap: var(--spacing);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="root"][data-orientation="horizontal"]) {
|
|
13
|
+
grid-template-rows: var(--cui-range-level-size);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="root"][data-orientation="horizontal"]:dir(rtl)) {
|
|
17
|
+
--_range-level-zone-angle: 270deg;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="root"][data-orientation="vertical"]) {
|
|
21
|
+
--_range-level-zone-angle: 0deg;
|
|
22
|
+
grid-auto-flow: column;
|
|
23
|
+
grid-template-columns: var(--cui-range-level-size);
|
|
24
|
+
grid-template-rows: minmax(0, 1fr);
|
|
25
|
+
inline-size: max-content;
|
|
26
|
+
min-block-size: calc(var(--spacing) * 16);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="track"]) {
|
|
30
|
+
border-radius: var(--cui-range-track-radius);
|
|
31
|
+
background: var(--cui-range-track-background);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="scale"]) {
|
|
35
|
+
block-size: calc(1lh + var(--spacing));
|
|
36
|
+
color: var(--cui-range-level-scale-foreground);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where(
|
|
40
|
+
[data-range-kind="level-meter"][data-slot="root"][data-orientation="vertical"]
|
|
41
|
+
[data-control-family="range"][data-range-kind="level-meter"][data-slot="scale"]
|
|
42
|
+
) {
|
|
43
|
+
block-size: auto;
|
|
44
|
+
inline-size: calc(var(--spacing) * 8);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="tick"]) {
|
|
48
|
+
position: absolute;
|
|
49
|
+
inset-block-start: 0;
|
|
50
|
+
inset-inline-start: calc(var(--_range-level-tick) * 100%);
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
align-items: center;
|
|
54
|
+
translate: -50% 0;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="tick"]:dir(rtl)) {
|
|
59
|
+
translate: 50% 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="tick"])::before {
|
|
63
|
+
content: "";
|
|
64
|
+
inline-size: 1px;
|
|
65
|
+
block-size: var(--spacing);
|
|
66
|
+
background: currentColor;
|
|
67
|
+
opacity: 0.5;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:where(
|
|
71
|
+
[data-range-kind="level-meter"][data-slot="root"][data-orientation="vertical"]
|
|
72
|
+
[data-control-family="range"][data-range-kind="level-meter"][data-slot="tick"]
|
|
73
|
+
) {
|
|
74
|
+
inset-block: auto calc(var(--_range-level-tick) * 100%);
|
|
75
|
+
inset-inline-start: 0;
|
|
76
|
+
flex-direction: row;
|
|
77
|
+
gap: calc(var(--spacing) * 1.5);
|
|
78
|
+
translate: 0 50%;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:where(
|
|
82
|
+
[data-range-kind="level-meter"][data-slot="root"][data-orientation="vertical"]
|
|
83
|
+
[data-control-family="range"][data-range-kind="level-meter"][data-slot="tick"]
|
|
84
|
+
)::before {
|
|
85
|
+
inline-size: var(--spacing);
|
|
86
|
+
block-size: 1px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="bar"]),
|
|
90
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="peak"]) {
|
|
91
|
+
background-image: linear-gradient(
|
|
92
|
+
var(--_range-level-zone-angle),
|
|
93
|
+
var(--cui-range-indicator-background) calc(var(--_range-level-warning-start, 1) * 100%),
|
|
94
|
+
var(--cui-range-level-warning-background) 0 calc(var(--_range-level-clip-start, 1) * 100%),
|
|
95
|
+
var(--cui-range-level-clip-background) 0
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="bar"]) {
|
|
100
|
+
clip-path: inset(0 calc((1 - var(--_range-level, 0)) * 100%) 0 0 round var(--cui-range-indicator-radius));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="peak"]) {
|
|
104
|
+
clip-path: inset(
|
|
105
|
+
0 calc((1 - var(--_range-level-peak, 0)) * 100%) 0 calc(var(--_range-level-peak, 0) * 100% - var(--_range-level-peak-size))
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="bar"]:dir(rtl)) {
|
|
110
|
+
clip-path: inset(0 0 0 calc((1 - var(--_range-level, 0)) * 100%) round var(--cui-range-indicator-radius));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:where([data-control-family="range"][data-range-kind="level-meter"][data-slot="peak"]:dir(rtl)) {
|
|
114
|
+
clip-path: inset(
|
|
115
|
+
0 calc(var(--_range-level-peak, 0) * 100% - var(--_range-level-peak-size)) 0 calc((1 - var(--_range-level-peak, 0)) * 100%)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:where(
|
|
120
|
+
[data-range-kind="level-meter"][data-slot="root"][data-orientation="vertical"]
|
|
121
|
+
[data-control-family="range"][data-range-kind="level-meter"][data-slot="bar"]
|
|
122
|
+
) {
|
|
123
|
+
clip-path: inset(calc((1 - var(--_range-level, 0)) * 100%) 0 0 0 round var(--cui-range-indicator-radius));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:where(
|
|
127
|
+
[data-range-kind="level-meter"][data-slot="root"][data-orientation="vertical"]
|
|
128
|
+
[data-control-family="range"][data-range-kind="level-meter"][data-slot="peak"]
|
|
129
|
+
) {
|
|
130
|
+
clip-path: inset(
|
|
131
|
+
calc((1 - var(--_range-level-peak, 0)) * 100%) 0 calc(var(--_range-level-peak, 0) * 100% - var(--_range-level-peak-size)) 0
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@property --cui-range-level-warning-background {
|
|
137
|
+
syntax: "<color>";
|
|
138
|
+
inherits: true;
|
|
139
|
+
initial-value: transparent;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@property --cui-range-level-clip-background {
|
|
143
|
+
syntax: "<color>";
|
|
144
|
+
inherits: true;
|
|
145
|
+
initial-value: transparent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@property --cui-range-level-size {
|
|
149
|
+
syntax: "<length-percentage>";
|
|
150
|
+
inherits: true;
|
|
151
|
+
initial-value: 0px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@property --cui-range-level-scale-foreground {
|
|
155
|
+
syntax: "<color>";
|
|
156
|
+
inherits: true;
|
|
157
|
+
initial-value: transparent;
|
|
158
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ChannelStripKnobStyle } from "../knob-contracts/channel-strip-knobs.js";
|
|
2
|
+
import { CSSProperties, ComponentProps } from "react";
|
|
3
|
+
//#region src/ui/channel-strip.d.ts
|
|
4
|
+
export type ChannelStripVariant = "default" | "master";
|
|
5
|
+
type ChannelStripPartProps = Omit<ComponentProps<"div">, "style"> & {
|
|
6
|
+
style?: CSSProperties & ChannelStripKnobStyle;
|
|
7
|
+
};
|
|
8
|
+
export type MixerProps = ChannelStripPartProps;
|
|
9
|
+
export type ChannelStripProps = ChannelStripPartProps & {
|
|
10
|
+
variant?: ChannelStripVariant;
|
|
11
|
+
};
|
|
12
|
+
export type ChannelStripHeaderProps = ChannelStripPartProps;
|
|
13
|
+
export type ChannelStripTitleProps = ComponentProps<"span">;
|
|
14
|
+
export type ChannelStripActionsProps = ChannelStripPartProps;
|
|
15
|
+
export type ChannelStripBodyProps = ChannelStripPartProps;
|
|
16
|
+
export type ChannelStripValueProps = ChannelStripPartProps;
|
|
17
|
+
export type ChannelStripControlsProps = ChannelStripPartProps;
|
|
18
|
+
export declare function Mixer({ className, ...props }: MixerProps): import("react").JSX.Element;
|
|
19
|
+
export declare function ChannelStrip({ variant, className, ...props }: ChannelStripProps): import("react").JSX.Element;
|
|
20
|
+
export declare function ChannelStripHeader({ className, ...props }: ChannelStripHeaderProps): import("react").JSX.Element;
|
|
21
|
+
export declare function ChannelStripTitle({ className, ...props }: ChannelStripTitleProps): import("react").JSX.Element;
|
|
22
|
+
export declare function ChannelStripActions({ className, ...props }: ChannelStripActionsProps): import("react").JSX.Element;
|
|
23
|
+
export declare function ChannelStripBody({ className, ...props }: ChannelStripBodyProps): import("react").JSX.Element;
|
|
24
|
+
export declare function ChannelStripValue({ className, ...props }: ChannelStripValueProps): import("react").JSX.Element;
|
|
25
|
+
export declare function ChannelStripControls({ className, ...props }: ChannelStripControlsProps): import("react").JSX.Element;
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { createContext, use, useId } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/ui/channel-strip.tsx
|
|
6
|
+
const ChannelStripTitleIdContext = createContext(null);
|
|
7
|
+
function Mixer({ className, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ jsx("div", {
|
|
9
|
+
"data-control-ui": "channel-strip",
|
|
10
|
+
"data-control-family": "channel-strip",
|
|
11
|
+
"data-slot": "mixer",
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function ChannelStrip({ variant = "default", className, ...props }) {
|
|
17
|
+
const titleId = useId();
|
|
18
|
+
return /* @__PURE__ */ jsx(ChannelStripTitleIdContext, {
|
|
19
|
+
value: titleId,
|
|
20
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
21
|
+
role: "group",
|
|
22
|
+
"aria-labelledby": titleId,
|
|
23
|
+
"data-control-ui": "channel-strip",
|
|
24
|
+
"data-control-family": "channel-strip",
|
|
25
|
+
"data-slot": "root",
|
|
26
|
+
"data-variant": variant,
|
|
27
|
+
className: cn("min-w-0", className),
|
|
28
|
+
...props
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function ChannelStripHeader({ className, ...props }) {
|
|
33
|
+
return /* @__PURE__ */ jsx("div", {
|
|
34
|
+
"data-control-ui": "channel-strip",
|
|
35
|
+
"data-control-family": "channel-strip",
|
|
36
|
+
"data-slot": "header",
|
|
37
|
+
className: cn("flex min-w-0 items-center", className),
|
|
38
|
+
...props
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function ChannelStripTitle({ className, ...props }) {
|
|
42
|
+
const titleId = use(ChannelStripTitleIdContext);
|
|
43
|
+
return /* @__PURE__ */ jsx("span", {
|
|
44
|
+
id: titleId ?? void 0,
|
|
45
|
+
"data-control-ui": "channel-strip",
|
|
46
|
+
"data-control-family": "channel-strip",
|
|
47
|
+
"data-slot": "title",
|
|
48
|
+
className: cn("min-w-0 flex-1 truncate text-caption", className),
|
|
49
|
+
...props
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function ChannelStripActions({ className, ...props }) {
|
|
53
|
+
return /* @__PURE__ */ jsx("div", {
|
|
54
|
+
"data-control-ui": "channel-strip",
|
|
55
|
+
"data-control-family": "channel-strip",
|
|
56
|
+
"data-slot": "actions",
|
|
57
|
+
className: cn("flex shrink-0 items-center", className),
|
|
58
|
+
...props
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function ChannelStripBody({ className, ...props }) {
|
|
62
|
+
return /* @__PURE__ */ jsx("div", {
|
|
63
|
+
"data-control-ui": "channel-strip",
|
|
64
|
+
"data-control-family": "channel-strip",
|
|
65
|
+
"data-slot": "body",
|
|
66
|
+
className: cn("flex items-stretch justify-center", className),
|
|
67
|
+
...props
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function ChannelStripValue({ className, ...props }) {
|
|
71
|
+
return /* @__PURE__ */ jsx("div", {
|
|
72
|
+
"data-control-ui": "channel-strip",
|
|
73
|
+
"data-control-family": "channel-strip",
|
|
74
|
+
"data-slot": "value",
|
|
75
|
+
className: cn("text-caption tabular-nums", className),
|
|
76
|
+
...props
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function ChannelStripControls({ className, ...props }) {
|
|
80
|
+
return /* @__PURE__ */ jsx("div", {
|
|
81
|
+
"data-control-ui": "channel-strip",
|
|
82
|
+
"data-control-family": "channel-strip",
|
|
83
|
+
"data-slot": "controls",
|
|
84
|
+
className: cn("flex flex-wrap content-start items-center justify-center", className),
|
|
85
|
+
...props
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { ChannelStrip, ChannelStripActions, ChannelStripBody, ChannelStripControls, ChannelStripHeader, ChannelStripTitle, ChannelStripValue, Mixer };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { RangeKnobStyle } from "../knob-contracts/range-knobs.js";
|
|
2
|
+
import { CSSProperties, ComponentProps } from "react";
|
|
3
|
+
//#region src/ui/level-meter.d.ts
|
|
4
|
+
export type LevelMeterSource = {
|
|
5
|
+
subscribe: (listener: (levelDb: number) => void) => () => void;
|
|
6
|
+
};
|
|
7
|
+
export type LevelMeterOrientation = "horizontal" | "vertical";
|
|
8
|
+
export type LevelMeterProps = Omit<ComponentProps<"div">, "style"> & {
|
|
9
|
+
value?: number;
|
|
10
|
+
source?: LevelMeterSource | null;
|
|
11
|
+
minDb?: number;
|
|
12
|
+
maxDb?: number;
|
|
13
|
+
warningDb?: number;
|
|
14
|
+
clipDb?: number;
|
|
15
|
+
orientation?: LevelMeterOrientation;
|
|
16
|
+
style?: CSSProperties & RangeKnobStyle;
|
|
17
|
+
};
|
|
18
|
+
export type LevelMeterTrackProps = Omit<ComponentProps<"div">, "style"> & {
|
|
19
|
+
style?: CSSProperties & RangeKnobStyle;
|
|
20
|
+
};
|
|
21
|
+
export type LevelMeterBarProps = Omit<ComponentProps<"div">, "style" | "children"> & {
|
|
22
|
+
style?: CSSProperties & RangeKnobStyle;
|
|
23
|
+
};
|
|
24
|
+
export type LevelMeterPeakProps = Omit<ComponentProps<"div">, "style" | "children"> & {
|
|
25
|
+
style?: CSSProperties & RangeKnobStyle;
|
|
26
|
+
};
|
|
27
|
+
export type LevelMeterScaleProps = Omit<ComponentProps<"div">, "style" | "children"> & {
|
|
28
|
+
ticks?: readonly number[];
|
|
29
|
+
style?: CSSProperties & RangeKnobStyle;
|
|
30
|
+
};
|
|
31
|
+
export declare function LevelMeter({ value, source, minDb, maxDb, warningDb, clipDb, orientation, className, style, children, ref, ...props }: LevelMeterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
32
|
+
export declare function LevelMeterTrack({ className, ...props }: LevelMeterTrackProps): import("react").JSX.Element;
|
|
33
|
+
export declare function LevelMeterBar({ className, ...props }: LevelMeterBarProps): import("react").JSX.Element;
|
|
34
|
+
export declare function LevelMeterPeak({ className, ...props }: LevelMeterPeakProps): import("react").JSX.Element;
|
|
35
|
+
export declare function LevelMeterScale({ ticks, className, ...props }: LevelMeterScaleProps): import("react").JSX.Element;
|
|
36
|
+
//#endregion
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { advanceLevelMeterReading, levelMeterFraction, levelMeterReadingSettled } from "../lib/level-meter-ballistics.js";
|
|
4
|
+
import { createContext, use, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/ui/level-meter.tsx
|
|
8
|
+
const ARIA_UPDATE_INTERVAL_MS = 250;
|
|
9
|
+
const STANDARD_TICKS_DB = [
|
|
10
|
+
12,
|
|
11
|
+
6,
|
|
12
|
+
0,
|
|
13
|
+
-6,
|
|
14
|
+
-12,
|
|
15
|
+
-18,
|
|
16
|
+
-24,
|
|
17
|
+
-36,
|
|
18
|
+
-48,
|
|
19
|
+
-60,
|
|
20
|
+
-72,
|
|
21
|
+
-84,
|
|
22
|
+
-96
|
|
23
|
+
];
|
|
24
|
+
const LevelMeterScaleContext = createContext(null);
|
|
25
|
+
function formatTickDb(db) {
|
|
26
|
+
if (db > 0) return `+${db}`;
|
|
27
|
+
if (db < 0) return `\u2212${Math.abs(db)}`;
|
|
28
|
+
return "0";
|
|
29
|
+
}
|
|
30
|
+
function createLevelMeterPainter() {
|
|
31
|
+
let attachment = null;
|
|
32
|
+
let targetDb = Number.NEGATIVE_INFINITY;
|
|
33
|
+
let reading = {
|
|
34
|
+
levelDb: Number.NEGATIVE_INFINITY,
|
|
35
|
+
peakDb: Number.NEGATIVE_INFINITY,
|
|
36
|
+
peakHeldUntilMs: 0
|
|
37
|
+
};
|
|
38
|
+
let frame = 0;
|
|
39
|
+
let lastFrameMs = 0;
|
|
40
|
+
let lastAnnouncedMs = Number.NEGATIVE_INFINITY;
|
|
41
|
+
const paint = ({ root, scale, announcesValue }, nowMs, settled) => {
|
|
42
|
+
root.style.setProperty("--_range-level", `${levelMeterFraction(reading.levelDb, scale)}`);
|
|
43
|
+
root.style.setProperty("--_range-level-peak", `${levelMeterFraction(reading.peakDb, scale)}`);
|
|
44
|
+
if (!announcesValue || !settled && nowMs - lastAnnouncedMs < ARIA_UPDATE_INTERVAL_MS) return;
|
|
45
|
+
lastAnnouncedMs = nowMs;
|
|
46
|
+
const roundedDb = Math.round(reading.levelDb);
|
|
47
|
+
root.setAttribute("aria-valuenow", `${roundedDb}`);
|
|
48
|
+
root.setAttribute("aria-valuetext", `${roundedDb} dB`);
|
|
49
|
+
};
|
|
50
|
+
const step = (nowMs) => {
|
|
51
|
+
frame = 0;
|
|
52
|
+
if (!attachment) return;
|
|
53
|
+
reading = advanceLevelMeterReading(reading, targetDb, nowMs - lastFrameMs, nowMs, attachment.scale);
|
|
54
|
+
lastFrameMs = nowMs;
|
|
55
|
+
const settled = levelMeterReadingSettled(reading, targetDb, attachment.scale);
|
|
56
|
+
paint(attachment, nowMs, settled);
|
|
57
|
+
if (!settled) frame = requestAnimationFrame(step);
|
|
58
|
+
};
|
|
59
|
+
const wake = () => {
|
|
60
|
+
if (frame !== 0 || !attachment) return;
|
|
61
|
+
lastFrameMs = performance.now();
|
|
62
|
+
frame = requestAnimationFrame(step);
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
attach(nextAttachment) {
|
|
66
|
+
attachment = nextAttachment;
|
|
67
|
+
wake();
|
|
68
|
+
return () => {
|
|
69
|
+
cancelAnimationFrame(frame);
|
|
70
|
+
frame = 0;
|
|
71
|
+
attachment = null;
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
setTarget(levelDb) {
|
|
75
|
+
targetDb = levelDb;
|
|
76
|
+
wake();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function LevelMeter({ value, source, minDb = -60, maxDb = 0, warningDb = -18, clipDb = -6, orientation = "horizontal", className, style, children, ref, ...props }) {
|
|
81
|
+
const [painter] = useState(createLevelMeterPainter);
|
|
82
|
+
const rootRef = useRef(null);
|
|
83
|
+
const labelled = Boolean(props["aria-label"] || props["aria-labelledby"]);
|
|
84
|
+
const scale = {
|
|
85
|
+
minDb,
|
|
86
|
+
maxDb
|
|
87
|
+
};
|
|
88
|
+
const zoneStyle = {
|
|
89
|
+
"--_range-level-warning-start": `${levelMeterFraction(warningDb, scale)}`,
|
|
90
|
+
"--_range-level-clip-start": `${levelMeterFraction(clipDb, scale)}`,
|
|
91
|
+
...style
|
|
92
|
+
};
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const root = rootRef.current;
|
|
95
|
+
if (!root) return;
|
|
96
|
+
return painter.attach({
|
|
97
|
+
root,
|
|
98
|
+
scale: {
|
|
99
|
+
minDb,
|
|
100
|
+
maxDb
|
|
101
|
+
},
|
|
102
|
+
announcesValue: labelled
|
|
103
|
+
});
|
|
104
|
+
}, [
|
|
105
|
+
painter,
|
|
106
|
+
minDb,
|
|
107
|
+
maxDb,
|
|
108
|
+
labelled
|
|
109
|
+
]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (source) return source.subscribe(painter.setTarget);
|
|
112
|
+
painter.setTarget(value ?? Number.NEGATIVE_INFINITY);
|
|
113
|
+
}, [
|
|
114
|
+
painter,
|
|
115
|
+
source,
|
|
116
|
+
value
|
|
117
|
+
]);
|
|
118
|
+
return useRender({
|
|
119
|
+
defaultTagName: "div",
|
|
120
|
+
ref: [ref ?? null, rootRef],
|
|
121
|
+
props: {
|
|
122
|
+
...props,
|
|
123
|
+
role: labelled ? "meter" : void 0,
|
|
124
|
+
"aria-hidden": labelled ? void 0 : true,
|
|
125
|
+
"aria-valuemin": labelled ? minDb : void 0,
|
|
126
|
+
"aria-valuemax": labelled ? maxDb : void 0,
|
|
127
|
+
"aria-valuenow": labelled ? minDb : void 0,
|
|
128
|
+
"data-control-ui": "level-meter",
|
|
129
|
+
"data-control-family": "range",
|
|
130
|
+
"data-range-kind": "level-meter",
|
|
131
|
+
"data-slot": "root",
|
|
132
|
+
"data-orientation": orientation,
|
|
133
|
+
className: cn("grid", className),
|
|
134
|
+
style: zoneStyle,
|
|
135
|
+
children: /* @__PURE__ */ jsx(LevelMeterScaleContext, {
|
|
136
|
+
value: {
|
|
137
|
+
minDb,
|
|
138
|
+
maxDb
|
|
139
|
+
},
|
|
140
|
+
children: children ?? /* @__PURE__ */ jsx(LevelMeterTrack, { children: /* @__PURE__ */ jsx(LevelMeterBar, {}) })
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function LevelMeterTrack({ className, ...props }) {
|
|
146
|
+
return /* @__PURE__ */ jsx("div", {
|
|
147
|
+
"data-control-ui": "level-meter",
|
|
148
|
+
"data-control-family": "range",
|
|
149
|
+
"data-range-kind": "level-meter",
|
|
150
|
+
"data-slot": "track",
|
|
151
|
+
className: cn("relative overflow-hidden", className),
|
|
152
|
+
...props
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function LevelMeterBar({ className, ...props }) {
|
|
156
|
+
return /* @__PURE__ */ jsx("div", {
|
|
157
|
+
"data-control-ui": "level-meter",
|
|
158
|
+
"data-control-family": "range",
|
|
159
|
+
"data-range-kind": "level-meter",
|
|
160
|
+
"data-slot": "bar",
|
|
161
|
+
className: cn("absolute inset-0", className),
|
|
162
|
+
...props
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function LevelMeterPeak({ className, ...props }) {
|
|
166
|
+
return /* @__PURE__ */ jsx("div", {
|
|
167
|
+
"data-control-ui": "level-meter",
|
|
168
|
+
"data-control-family": "range",
|
|
169
|
+
"data-range-kind": "level-meter",
|
|
170
|
+
"data-slot": "peak",
|
|
171
|
+
className: cn("absolute inset-0", className),
|
|
172
|
+
...props
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function LevelMeterScale({ ticks, className, ...props }) {
|
|
176
|
+
const scale = use(LevelMeterScaleContext);
|
|
177
|
+
if (!scale) throw new Error("LevelMeterScale must be rendered inside LevelMeter.");
|
|
178
|
+
const visibleTicks = (ticks ?? STANDARD_TICKS_DB).filter((db) => db >= scale.minDb && db <= scale.maxDb);
|
|
179
|
+
return /* @__PURE__ */ jsx("div", {
|
|
180
|
+
"aria-hidden": "true",
|
|
181
|
+
"data-control-ui": "level-meter",
|
|
182
|
+
"data-control-family": "range",
|
|
183
|
+
"data-range-kind": "level-meter",
|
|
184
|
+
"data-slot": "scale",
|
|
185
|
+
className: cn("relative text-micro tabular-nums", className),
|
|
186
|
+
...props,
|
|
187
|
+
children: visibleTicks.map((db) => {
|
|
188
|
+
const tickStyle = { "--_range-level-tick": `${levelMeterFraction(db, scale)}` };
|
|
189
|
+
return /* @__PURE__ */ jsx("span", {
|
|
190
|
+
"data-control-ui": "level-meter",
|
|
191
|
+
"data-control-family": "range",
|
|
192
|
+
"data-range-kind": "level-meter",
|
|
193
|
+
"data-slot": "tick",
|
|
194
|
+
style: tickStyle,
|
|
195
|
+
children: formatTickDb(db)
|
|
196
|
+
}, db);
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
export { LevelMeter, LevelMeterBar, LevelMeterPeak, LevelMeterScale, LevelMeterTrack };
|