@ctrl-ui/react 0.8.14 → 0.8.16
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 +38 -0
- package/dist/audio-device-select.js +183 -0
- package/dist/audio-visualizer-bar.d.ts +10 -0
- package/dist/audio-visualizer-bar.js +49 -0
- package/dist/audio-visualizer-line.d.ts +4 -0
- package/dist/audio-visualizer-line.js +75 -0
- package/dist/audio-visualizer.d.ts +1 -1
- package/dist/audio-visualizer.js +43 -16
- package/dist/hooks/use-audio-recorder.d.ts +1 -0
- package/dist/hooks/use-audio-recorder.js +27 -4
- package/dist/hooks/use-textarea-trigger-menu.js +5 -2
- package/dist/knob-contracts/audio-device-select-knobs.d.ts +4 -0
- package/dist/knob-contracts/audio-device-select-knobs.js +4 -0
- package/dist/knob-contracts/audio-visualizer-knobs.d.ts +1 -1
- package/dist/knob-contracts/audio-visualizer-knobs.js +2 -0
- package/dist/lib/audio-visualizer-levels.d.ts +12 -0
- package/dist/lib/audio-visualizer-levels.js +37 -0
- package/dist/model-switcher.d.ts +23 -0
- package/dist/model-switcher.js +70 -0
- package/dist/styles/index.css +3 -2
- package/dist/styles/recipes/audio-device-select.css +15 -0
- package/dist/styles/recipes/audio-visualizer.css +97 -2
- package/dist/styles/theme.css +8 -0
- package/dist/ui/autocomplete.d.ts +1 -1
- package/dist/ui/color-picker.js +1 -1
- package/dist/ui/combobox.d.ts +1 -1
- package/dist/ui/context-menu.js +1 -1
- package/dist/ui/dropdown-menu.js +1 -1
- package/dist/ui/gradient-editor.js +1 -1
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.js +1 -1
- package/dist/ui/sidebar-resize-rail.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Button } from "./ui/button.js";
|
|
2
|
+
import { AudioDeviceSelectKnobStyle } from "./knob-contracts/audio-device-select-knobs.js";
|
|
3
|
+
import { SelectContent, SelectItem, SelectProps, SelectTriggerProps } from "./ui/select.js";
|
|
4
|
+
import { CSSProperties, ComponentProps, ReactNode } from "react";
|
|
5
|
+
//#region src/audio-device-select.d.ts
|
|
6
|
+
export type AudioDevice = {
|
|
7
|
+
deviceId: string;
|
|
8
|
+
label: string;
|
|
9
|
+
isDefault?: boolean;
|
|
10
|
+
status?: "available" | "unavailable" | "permission-required";
|
|
11
|
+
};
|
|
12
|
+
export type AudioDevicePermission = "granted" | "prompt" | "denied";
|
|
13
|
+
export type AudioDeviceSelectProps = Omit<SelectProps, "items" | "children"> & {
|
|
14
|
+
devices: readonly AudioDevice[];
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
permission?: AudioDevicePermission;
|
|
17
|
+
onRequestPermission?: () => void | Promise<void>;
|
|
18
|
+
allowNone?: boolean;
|
|
19
|
+
noneLabel?: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
error?: string | null;
|
|
22
|
+
className?: string;
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
};
|
|
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;
|
|
26
|
+
export declare function AudioDeviceSelectTrigger({ children, className, ...props }: SelectTriggerProps): import("react").JSX.Element;
|
|
27
|
+
export declare function AudioDeviceSelectValue({ placeholder }: {
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
export declare function AudioDeviceSelectContent({ children, ...props }: ComponentProps<typeof SelectContent>): import("react").JSX.Element;
|
|
31
|
+
export declare function AudioDeviceSelectItem(props: ComponentProps<typeof SelectItem>): import("react").JSX.Element;
|
|
32
|
+
export declare function AudioDeviceSelectPreview({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
|
|
33
|
+
export type AudioDeviceSelectStatusProps = Omit<ComponentProps<"p">, "style"> & {
|
|
34
|
+
style?: CSSProperties & AudioDeviceSelectKnobStyle;
|
|
35
|
+
};
|
|
36
|
+
export declare function AudioDeviceSelectStatus({ className, ...props }: AudioDeviceSelectStatusProps): import("react").JSX.Element | null;
|
|
37
|
+
export declare function AudioDeviceSelectPermission({ children, onClick, ...props }: ComponentProps<typeof Button>): import("react").JSX.Element | null;
|
|
38
|
+
//#endregion
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "./lib/cn.js";
|
|
3
|
+
import { Button } from "./ui/button.js";
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select.js";
|
|
5
|
+
import { createContext, use, useState } from "react";
|
|
6
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { MicIcon } from "lucide-react";
|
|
8
|
+
//#region src/audio-device-select.tsx
|
|
9
|
+
const AudioDeviceSelectContext = createContext(null);
|
|
10
|
+
function useAudioDeviceSelect() {
|
|
11
|
+
const context = use(AudioDeviceSelectContext);
|
|
12
|
+
if (!context) throw new Error("AudioDeviceSelect parts must be rendered inside AudioDeviceSelect.");
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
function deviceLabel(device, index = 0) {
|
|
16
|
+
return device.label.trim() || `Microphone ${index + 1}`;
|
|
17
|
+
}
|
|
18
|
+
function AudioDeviceSelect({ devices, value, defaultValue, onValueChange, disabled = false, loading = false, permission = "granted", onRequestPermission, allowNone = false, noneLabel = "No microphone", label = "Microphone", error = null, className, children, ...props }) {
|
|
19
|
+
const [internalValue, setInternalValue] = useState(() => defaultValue ?? null);
|
|
20
|
+
const current = value === void 0 ? internalValue : value;
|
|
21
|
+
const connected = devices.find((device) => device.deviceId === current) ?? null;
|
|
22
|
+
const [remembered, setRemembered] = useState(connected);
|
|
23
|
+
if (connected && (remembered?.deviceId !== connected.deviceId || remembered?.label !== connected.label)) setRemembered(connected);
|
|
24
|
+
const missing = current !== null && !(allowNone && current === "") && !connected && !loading;
|
|
25
|
+
const selected = connected ?? (remembered?.deviceId === current ? remembered : null);
|
|
26
|
+
function handleValueChange(next) {
|
|
27
|
+
setInternalValue(next);
|
|
28
|
+
onValueChange?.(next);
|
|
29
|
+
}
|
|
30
|
+
return /* @__PURE__ */ jsx(AudioDeviceSelectContext.Provider, {
|
|
31
|
+
value: {
|
|
32
|
+
devices,
|
|
33
|
+
selected,
|
|
34
|
+
value: current,
|
|
35
|
+
missing,
|
|
36
|
+
loading,
|
|
37
|
+
permission,
|
|
38
|
+
disabled,
|
|
39
|
+
allowNone,
|
|
40
|
+
noneLabel,
|
|
41
|
+
label,
|
|
42
|
+
error,
|
|
43
|
+
onRequestPermission
|
|
44
|
+
},
|
|
45
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
46
|
+
"data-control-ui": "audio-device-select",
|
|
47
|
+
"data-control-family": "audio-device-select",
|
|
48
|
+
"data-slot": "root",
|
|
49
|
+
className: cn("grid min-w-0 gap-2", className),
|
|
50
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
51
|
+
value: current,
|
|
52
|
+
onValueChange: handleValueChange,
|
|
53
|
+
disabled: disabled || loading,
|
|
54
|
+
...props,
|
|
55
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
56
|
+
/* @__PURE__ */ jsx(AudioDeviceSelectTrigger, {}),
|
|
57
|
+
/* @__PURE__ */ jsx(AudioDeviceSelectContent, {}),
|
|
58
|
+
/* @__PURE__ */ jsx(AudioDeviceSelectStatus, {}),
|
|
59
|
+
/* @__PURE__ */ jsx(AudioDeviceSelectPermission, {})
|
|
60
|
+
] })
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function AudioDeviceSelectTrigger({ children, className, ...props }) {
|
|
66
|
+
const context = useAudioDeviceSelect();
|
|
67
|
+
return /* @__PURE__ */ jsx(SelectTrigger, {
|
|
68
|
+
"data-control-ui": "audio-device-select",
|
|
69
|
+
"data-slot": "trigger",
|
|
70
|
+
"data-loading": context.loading ? "true" : void 0,
|
|
71
|
+
"data-permission": context.permission,
|
|
72
|
+
"data-missing": context.missing ? "true" : void 0,
|
|
73
|
+
"aria-label": context.label,
|
|
74
|
+
"aria-busy": context.loading || void 0,
|
|
75
|
+
className: cn("w-full", className),
|
|
76
|
+
...props,
|
|
77
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MicIcon, {
|
|
78
|
+
"aria-hidden": "true",
|
|
79
|
+
className: "size-4 shrink-0"
|
|
80
|
+
}), /* @__PURE__ */ jsx(AudioDeviceSelectValue, {})] })
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function AudioDeviceSelectValue({ placeholder = "Select a microphone" }) {
|
|
84
|
+
const context = useAudioDeviceSelect();
|
|
85
|
+
let text = placeholder;
|
|
86
|
+
if (context.selected) text = deviceLabel(context.selected, context.devices.indexOf(context.selected));
|
|
87
|
+
if (context.allowNone && context.value === "") text = context.noneLabel;
|
|
88
|
+
if (context.missing) text = `${context.selected ? deviceLabel(context.selected) : "Unknown device"} (disconnected)`;
|
|
89
|
+
if (context.loading) text = "Finding devices…";
|
|
90
|
+
return /* @__PURE__ */ jsx("span", {
|
|
91
|
+
"data-control-ui": "audio-device-select",
|
|
92
|
+
"data-slot": "value",
|
|
93
|
+
className: "min-w-0 truncate",
|
|
94
|
+
title: text,
|
|
95
|
+
children: text
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function AudioDeviceSelectContent({ children, ...props }) {
|
|
99
|
+
const context = useAudioDeviceSelect();
|
|
100
|
+
return /* @__PURE__ */ jsx(SelectContent, {
|
|
101
|
+
"data-control-ui": "audio-device-select",
|
|
102
|
+
"data-slot": "content",
|
|
103
|
+
...props,
|
|
104
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
105
|
+
context.allowNone ? /* @__PURE__ */ jsx(SelectItem, {
|
|
106
|
+
value: "",
|
|
107
|
+
children: context.noneLabel
|
|
108
|
+
}) : null,
|
|
109
|
+
context.missing ? /* @__PURE__ */ jsx(SelectItem, {
|
|
110
|
+
value: context.value ?? "",
|
|
111
|
+
disabled: true,
|
|
112
|
+
children: /* @__PURE__ */ jsx(AudioDeviceSelectValue, {})
|
|
113
|
+
}) : null,
|
|
114
|
+
context.devices.filter((device) => !context.allowNone || device.deviceId !== "").map((device, index) => /* @__PURE__ */ jsxs(AudioDeviceSelectItem, {
|
|
115
|
+
value: device.deviceId,
|
|
116
|
+
label: deviceLabel(device, index),
|
|
117
|
+
disabled: device.status === "unavailable" || device.status === "permission-required",
|
|
118
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
119
|
+
className: "min-w-0 flex-1 truncate",
|
|
120
|
+
children: deviceLabel(device, index)
|
|
121
|
+
}), device.isDefault || device.deviceId === "default" ? /* @__PURE__ */ jsx("span", {
|
|
122
|
+
className: "ms-3 text-caption text-muted-foreground",
|
|
123
|
+
children: "Default"
|
|
124
|
+
}) : null]
|
|
125
|
+
}, device.deviceId)),
|
|
126
|
+
context.devices.length === 0 && !context.missing && !context.allowNone ? /* @__PURE__ */ jsx(SelectItem, {
|
|
127
|
+
value: "__audio-device-empty__",
|
|
128
|
+
disabled: true,
|
|
129
|
+
children: context.permission === "granted" ? "No microphones found" : "Allow microphone access to find devices"
|
|
130
|
+
}) : null
|
|
131
|
+
] })
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function AudioDeviceSelectItem(props) {
|
|
135
|
+
return /* @__PURE__ */ jsx(SelectItem, {
|
|
136
|
+
"data-control-ui": "audio-device-select",
|
|
137
|
+
"data-slot": "item",
|
|
138
|
+
...props
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function AudioDeviceSelectPreview({ className, ...props }) {
|
|
142
|
+
return /* @__PURE__ */ jsx("div", {
|
|
143
|
+
"data-control-ui": "audio-device-select",
|
|
144
|
+
"data-slot": "preview",
|
|
145
|
+
className: cn("min-w-0", className),
|
|
146
|
+
...props
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function AudioDeviceSelectStatus({ className, ...props }) {
|
|
150
|
+
const context = useAudioDeviceSelect();
|
|
151
|
+
let message = context.error;
|
|
152
|
+
if (!message && context.permission === "denied") message = "Microphone access is blocked. Allow it in your browser’s site settings.";
|
|
153
|
+
if (!message && context.missing) message = "This microphone is disconnected. Reconnect it or choose another device.";
|
|
154
|
+
if (!message) return null;
|
|
155
|
+
return /* @__PURE__ */ jsx("p", {
|
|
156
|
+
"data-control-ui": "audio-device-select",
|
|
157
|
+
"data-control-family": "audio-device-select",
|
|
158
|
+
"data-slot": "status",
|
|
159
|
+
role: "status",
|
|
160
|
+
className: cn("text-caption", className),
|
|
161
|
+
...props,
|
|
162
|
+
children: message
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function AudioDeviceSelectPermission({ children = "Allow microphone access", onClick, ...props }) {
|
|
166
|
+
const context = useAudioDeviceSelect();
|
|
167
|
+
if (context.permission !== "prompt" || !context.onRequestPermission) return null;
|
|
168
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
169
|
+
"data-control-ui": "audio-device-select",
|
|
170
|
+
"data-slot": "permission",
|
|
171
|
+
variant: "surface",
|
|
172
|
+
size: "sm",
|
|
173
|
+
disabled: context.disabled || context.loading,
|
|
174
|
+
onClick: (event) => {
|
|
175
|
+
onClick?.(event);
|
|
176
|
+
if (!event.defaultPrevented) context.onRequestPermission?.();
|
|
177
|
+
},
|
|
178
|
+
...props,
|
|
179
|
+
children
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { AudioDeviceSelect, AudioDeviceSelectContent, AudioDeviceSelectItem, AudioDeviceSelectPermission, AudioDeviceSelectPreview, AudioDeviceSelectStatus, AudioDeviceSelectTrigger, AudioDeviceSelectValue };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AudioVisualizerProps } from "./audio-visualizer.js";
|
|
2
|
+
//#region src/audio-visualizer-bar.d.ts
|
|
3
|
+
export type BarAudioVisualizerProps = AudioVisualizerProps & {
|
|
4
|
+
align?: "center" | "start" | "end";
|
|
5
|
+
mirrored?: boolean;
|
|
6
|
+
idle?: "static" | "pulse" | "wave";
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function AudioVisualizer({ levels, points, active, align, mirrored, idle, loading, className, style, ...props }: BarAudioVisualizerProps): import("react").JSX.Element;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cn } from "./lib/cn.js";
|
|
2
|
+
import { audioVisualizerBands } from "./lib/audio-visualizer-levels.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/audio-visualizer-bar.tsx
|
|
5
|
+
function AudioVisualizer({ levels, points, active = true, align = "center", mirrored = false, idle = "static", loading = false, className, style, ...props }) {
|
|
6
|
+
const visible = audioVisualizerBands(levels, points, active && !loading, mirrored);
|
|
7
|
+
const hasSignal = active && !loading && visible.some(({ level }) => level > 0);
|
|
8
|
+
const labelled = Boolean(props["aria-label"] || props["aria-labelledby"]);
|
|
9
|
+
return /* @__PURE__ */ jsx("div", {
|
|
10
|
+
"data-control-ui": "audio-visualizer",
|
|
11
|
+
"data-control-family": "audio-visualizer",
|
|
12
|
+
"data-slot": "root",
|
|
13
|
+
"data-variant": "bar",
|
|
14
|
+
"data-align": align,
|
|
15
|
+
"data-mirrored": mirrored ? "true" : void 0,
|
|
16
|
+
"data-active": hasSignal ? "true" : void 0,
|
|
17
|
+
"data-loading": loading ? "true" : void 0,
|
|
18
|
+
"data-idle": idle,
|
|
19
|
+
role: labelled ? "img" : void 0,
|
|
20
|
+
"aria-hidden": labelled ? void 0 : true,
|
|
21
|
+
"aria-busy": loading || void 0,
|
|
22
|
+
className: cn("shrink-0 overflow-hidden", className),
|
|
23
|
+
style,
|
|
24
|
+
...props,
|
|
25
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
26
|
+
"data-control-ui": "audio-visualizer",
|
|
27
|
+
"data-control-family": "audio-visualizer",
|
|
28
|
+
"data-slot": "track",
|
|
29
|
+
"data-active": hasSignal || loading ? "true" : void 0,
|
|
30
|
+
children: visible.map(({ key, level }, index) => {
|
|
31
|
+
const amplitude = Math.sqrt(level);
|
|
32
|
+
const barStyle = {
|
|
33
|
+
height: `${(.08 + amplitude * .92) * 100}%`,
|
|
34
|
+
"--_audio-visualizer-level-opacity": `${.48 + amplitude * .52}`,
|
|
35
|
+
"--_audio-visualizer-delay": `${-index * 70}ms`
|
|
36
|
+
};
|
|
37
|
+
return /* @__PURE__ */ jsx("span", {
|
|
38
|
+
"data-control-ui": "audio-visualizer",
|
|
39
|
+
"data-control-family": "audio-visualizer",
|
|
40
|
+
"data-slot": "bar",
|
|
41
|
+
"data-index": index,
|
|
42
|
+
style: barStyle
|
|
43
|
+
}, key);
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { AudioVisualizer };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cn } from "./lib/cn.js";
|
|
2
|
+
import { audioVisualizerHistory } from "./lib/audio-visualizer-levels.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/audio-visualizer-line.tsx
|
|
5
|
+
const LINE_VIEWBOX_WIDTH = 100;
|
|
6
|
+
const LINE_VIEWBOX_HEIGHT = 32;
|
|
7
|
+
const LINE_CENTER = LINE_VIEWBOX_HEIGHT / 2;
|
|
8
|
+
const CURVE_TENSION = .18;
|
|
9
|
+
function smoothPath(points) {
|
|
10
|
+
const [first, ...rest] = points;
|
|
11
|
+
if (!first) return "";
|
|
12
|
+
return rest.reduce((path, point, index) => {
|
|
13
|
+
const previous = points[index] ?? first;
|
|
14
|
+
const controlOffset = (point.x - previous.x) * CURVE_TENSION;
|
|
15
|
+
return `${path} C ${(previous.x + controlOffset).toFixed(2)} ${previous.y.toFixed(2)}, ${(point.x - controlOffset).toFixed(2)} ${point.y.toFixed(2)}, ${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
|
|
16
|
+
}, `M ${first.x.toFixed(2)} ${first.y.toFixed(2)}`);
|
|
17
|
+
}
|
|
18
|
+
function envelopePath(levels) {
|
|
19
|
+
const envelope = levels.length === 1 ? [levels[0] ?? 0, levels[0] ?? 0] : levels;
|
|
20
|
+
const step = LINE_VIEWBOX_WIDTH / Math.max(1, envelope.length - 1);
|
|
21
|
+
const upper = envelope.map((level, index) => ({
|
|
22
|
+
x: index * step,
|
|
23
|
+
y: 16 - Math.sqrt(level) * 13
|
|
24
|
+
}));
|
|
25
|
+
const lowerReversed = [...envelope.map((level, index) => ({
|
|
26
|
+
x: index * step,
|
|
27
|
+
y: 16 + Math.sqrt(level) * 13
|
|
28
|
+
}))].reverse();
|
|
29
|
+
return `${smoothPath(upper)} L ${lowerReversed[0]?.x.toFixed(2) ?? 0} ${lowerReversed[0]?.y.toFixed(2) ?? LINE_CENTER}${smoothPath(lowerReversed).replace(/^M [^C]+/, "")} Z`;
|
|
30
|
+
}
|
|
31
|
+
function AudioVisualizer({ levels, points, active = true, className, style, ...props }) {
|
|
32
|
+
const path = envelopePath(audioVisualizerHistory(levels, points, active).map(({ level }) => level));
|
|
33
|
+
const pathStyle = { d: `path('${path}')` };
|
|
34
|
+
const labelled = Boolean(props["aria-label"] || props["aria-labelledby"]);
|
|
35
|
+
return /* @__PURE__ */ jsx("div", {
|
|
36
|
+
"data-control-ui": "audio-visualizer",
|
|
37
|
+
"data-control-family": "audio-visualizer",
|
|
38
|
+
"data-slot": "root",
|
|
39
|
+
"data-variant": "line",
|
|
40
|
+
"data-active": active ? "true" : void 0,
|
|
41
|
+
role: labelled ? "img" : void 0,
|
|
42
|
+
"aria-hidden": labelled ? void 0 : true,
|
|
43
|
+
className: cn("shrink-0 overflow-hidden", className),
|
|
44
|
+
style,
|
|
45
|
+
...props,
|
|
46
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
47
|
+
"data-control-ui": "audio-visualizer",
|
|
48
|
+
"data-control-family": "audio-visualizer",
|
|
49
|
+
"data-slot": "track",
|
|
50
|
+
"data-active": active ? "true" : void 0,
|
|
51
|
+
viewBox: `0 0 ${LINE_VIEWBOX_WIDTH} ${LINE_VIEWBOX_HEIGHT}`,
|
|
52
|
+
preserveAspectRatio: "none",
|
|
53
|
+
"aria-hidden": "true",
|
|
54
|
+
className: "size-full overflow-visible",
|
|
55
|
+
children: [/* @__PURE__ */ jsx("path", {
|
|
56
|
+
"data-control-ui": "audio-visualizer",
|
|
57
|
+
"data-control-family": "audio-visualizer",
|
|
58
|
+
"data-slot": "baseline",
|
|
59
|
+
d: `M 0 ${LINE_CENTER} H ${LINE_VIEWBOX_WIDTH}`,
|
|
60
|
+
vectorEffect: "non-scaling-stroke"
|
|
61
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
62
|
+
"data-control-ui": "audio-visualizer-line",
|
|
63
|
+
"data-control-family": "audio-visualizer",
|
|
64
|
+
"data-slot": "waveform",
|
|
65
|
+
d: path,
|
|
66
|
+
style: pathStyle,
|
|
67
|
+
strokeWidth: "1",
|
|
68
|
+
strokeLinejoin: "round",
|
|
69
|
+
vectorEffect: "non-scaling-stroke"
|
|
70
|
+
})]
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { AudioVisualizer };
|
|
@@ -2,7 +2,7 @@ import { AudioVisualizerKnobStyle } from "./knob-contracts/audio-visualizer-knob
|
|
|
2
2
|
import { CSSProperties, ComponentProps } from "react";
|
|
3
3
|
//#region src/audio-visualizer.d.ts
|
|
4
4
|
export type AudioVisualizerProps = Omit<ComponentProps<"div">, "children" | "style"> & {
|
|
5
|
-
levels:
|
|
5
|
+
levels: ArrayLike<number>;
|
|
6
6
|
active?: boolean;
|
|
7
7
|
points?: number;
|
|
8
8
|
style?: CSSProperties & AudioVisualizerKnobStyle;
|
package/dist/audio-visualizer.js
CHANGED
|
@@ -1,47 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { cn } from "./lib/cn.js";
|
|
3
|
+
import { audioVisualizerHistory } from "./lib/audio-visualizer-levels.js";
|
|
4
|
+
import { useLayoutEffect, useRef } from "react";
|
|
2
5
|
import { jsx } from "react/jsx-runtime";
|
|
3
6
|
//#region src/audio-visualizer.tsx
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
return (visible.length === pointCount ? visible : [...new Array(pointCount - visible.length).fill(0), ...visible]).map((level, position) => ({
|
|
11
|
-
key: `bar-${position}`,
|
|
12
|
-
level
|
|
13
|
-
}));
|
|
7
|
+
const MIN_VISIBLE_LEVEL = .04;
|
|
8
|
+
function historyAdvanced(previous, next) {
|
|
9
|
+
if (!previous?.active || !next.active || previous.levels.length !== next.levels.length || next.levels.length < 2) return false;
|
|
10
|
+
const moved = previous.levels.slice(1).every((level, index) => level === next.levels[index]);
|
|
11
|
+
const changed = previous.levels.some((level, index) => level !== next.levels[index]);
|
|
12
|
+
return moved && changed;
|
|
14
13
|
}
|
|
15
14
|
function AudioVisualizer({ levels, points, active = true, className, style, ...props }) {
|
|
16
|
-
const visible =
|
|
15
|
+
const visible = audioVisualizerHistory(levels, points, active);
|
|
16
|
+
const labelled = Boolean(props["aria-label"] || props["aria-labelledby"]);
|
|
17
|
+
const trackRef = useRef(null);
|
|
18
|
+
const firstBarRef = useRef(null);
|
|
19
|
+
const previousSample = useRef(null);
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
const next = {
|
|
22
|
+
levels: visible.map(({ level }) => level),
|
|
23
|
+
time: performance.now(),
|
|
24
|
+
active
|
|
25
|
+
};
|
|
26
|
+
const previous = previousSample.current;
|
|
27
|
+
previousSample.current = next;
|
|
28
|
+
const track = trackRef.current;
|
|
29
|
+
const firstBar = firstBarRef.current;
|
|
30
|
+
if (!track || !firstBar || !historyAdvanced(previous, next) || document.hidden) return;
|
|
31
|
+
const duration = next.time - (previous?.time ?? next.time);
|
|
32
|
+
if (duration < 16 || duration > 250 || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
33
|
+
const direction = getComputedStyle(track).direction === "rtl" ? -1 : 1;
|
|
34
|
+
const pitch = (track.clientWidth - firstBar.clientWidth) / (visible.length - 1);
|
|
35
|
+
const animation = track.animate([{ transform: `translateX(${pitch * direction}px)` }, { transform: "translateX(0)" }], {
|
|
36
|
+
duration,
|
|
37
|
+
easing: "linear"
|
|
38
|
+
});
|
|
39
|
+
return () => animation.cancel();
|
|
40
|
+
}, [visible, active]);
|
|
17
41
|
return /* @__PURE__ */ jsx("div", {
|
|
18
42
|
"data-control-ui": "audio-visualizer",
|
|
19
43
|
"data-control-family": "audio-visualizer",
|
|
20
44
|
"data-slot": "root",
|
|
21
45
|
"data-variant": "bars",
|
|
22
46
|
"data-active": active ? "true" : void 0,
|
|
23
|
-
"
|
|
24
|
-
|
|
47
|
+
role: labelled ? "img" : void 0,
|
|
48
|
+
"aria-hidden": labelled ? void 0 : true,
|
|
49
|
+
className: cn("shrink-0 overflow-hidden mask-x-from-90%", className),
|
|
25
50
|
style,
|
|
26
51
|
...props,
|
|
27
52
|
children: /* @__PURE__ */ jsx("span", {
|
|
53
|
+
ref: trackRef,
|
|
28
54
|
"data-control-ui": "audio-visualizer",
|
|
29
55
|
"data-control-family": "audio-visualizer",
|
|
30
56
|
"data-slot": "track",
|
|
31
57
|
"data-active": active ? "true" : void 0,
|
|
32
|
-
className: "flex size-full items-stretch justify-
|
|
58
|
+
className: "flex size-full items-stretch justify-between",
|
|
33
59
|
children: visible.map(({ key, level }) => {
|
|
34
60
|
const perceptualLevel = Math.sqrt(level);
|
|
35
|
-
const visibleLevel = MIN_VISIBLE_LEVEL + perceptualLevel * .
|
|
61
|
+
const visibleLevel = MIN_VISIBLE_LEVEL + perceptualLevel * .96;
|
|
36
62
|
const levelStyle = {
|
|
37
63
|
"--_audio-visualizer-level-opacity": `${.48 + perceptualLevel * .52}`,
|
|
38
64
|
clipPath: `inset(${(1 - visibleLevel) * 50}% 0 round var(--cui-audio-visualizer-bar-radius))`
|
|
39
65
|
};
|
|
40
66
|
return /* @__PURE__ */ jsx("span", {
|
|
67
|
+
ref: key === "bar-0" ? firstBarRef : void 0,
|
|
41
68
|
"data-control-ui": "audio-visualizer",
|
|
42
69
|
"data-control-family": "audio-visualizer",
|
|
43
70
|
"data-slot": "bar-track",
|
|
44
|
-
className: "flex
|
|
71
|
+
className: "flex items-center",
|
|
45
72
|
children: /* @__PURE__ */ jsx("span", {
|
|
46
73
|
"data-control-ui": "audio-visualizer",
|
|
47
74
|
"data-control-family": "audio-visualizer",
|
|
@@ -27,14 +27,14 @@ function useAudioInputDevices() {
|
|
|
27
27
|
const [devices, setDevices] = useState([]);
|
|
28
28
|
const [loading, setLoading] = useState(false);
|
|
29
29
|
const [error, setError] = useState(null);
|
|
30
|
-
const [
|
|
30
|
+
const [permission, setPermission] = useState("prompt");
|
|
31
31
|
async function refresh() {
|
|
32
32
|
setLoading(true);
|
|
33
33
|
setError(null);
|
|
34
34
|
try {
|
|
35
35
|
const nextDevices = await enumerateAudioInputDevices();
|
|
36
36
|
setDevices(nextDevices);
|
|
37
|
-
if (nextDevices.some((device) => device.label))
|
|
37
|
+
if (nextDevices.some((device) => device.label)) setPermission("granted");
|
|
38
38
|
} catch (nextError) {
|
|
39
39
|
setError(asError(nextError));
|
|
40
40
|
} finally {
|
|
@@ -51,9 +51,11 @@ function useAudioInputDevices() {
|
|
|
51
51
|
try {
|
|
52
52
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
53
53
|
for (const track of stream.getTracks()) track.stop();
|
|
54
|
-
|
|
54
|
+
setPermission("granted");
|
|
55
55
|
setDevices(await enumerateAudioInputDevices());
|
|
56
56
|
} catch (nextError) {
|
|
57
|
+
const failure = asError(nextError);
|
|
58
|
+
if (failure.name === "NotAllowedError" || failure.name === "SecurityError") setPermission("denied");
|
|
57
59
|
setError(microphoneError(nextError));
|
|
58
60
|
} finally {
|
|
59
61
|
setLoading(false);
|
|
@@ -63,6 +65,26 @@ function useAudioInputDevices() {
|
|
|
63
65
|
useEffect(() => {
|
|
64
66
|
queueMicrotask(() => void refreshFromEffect());
|
|
65
67
|
}, []);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (typeof navigator === "undefined" || !navigator.permissions?.query) return;
|
|
70
|
+
let disposed = false;
|
|
71
|
+
let status;
|
|
72
|
+
function updatePermission() {
|
|
73
|
+
if (disposed || !status) return;
|
|
74
|
+
setPermission(status.state);
|
|
75
|
+
refreshFromEffect();
|
|
76
|
+
}
|
|
77
|
+
navigator.permissions.query({ name: "microphone" }).then((nextStatus) => {
|
|
78
|
+
if (disposed) return;
|
|
79
|
+
status = nextStatus;
|
|
80
|
+
setPermission(nextStatus.state);
|
|
81
|
+
status.addEventListener("change", updatePermission);
|
|
82
|
+
}).catch(() => {});
|
|
83
|
+
return () => {
|
|
84
|
+
disposed = true;
|
|
85
|
+
status?.removeEventListener("change", updatePermission);
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
66
88
|
useEffect(() => {
|
|
67
89
|
if (typeof navigator === "undefined" || !navigator.mediaDevices?.addEventListener) return;
|
|
68
90
|
const handleDeviceChange = () => {
|
|
@@ -75,7 +97,8 @@ function useAudioInputDevices() {
|
|
|
75
97
|
devices,
|
|
76
98
|
loading,
|
|
77
99
|
error,
|
|
78
|
-
hasPermission,
|
|
100
|
+
hasPermission: permission === "granted",
|
|
101
|
+
permission,
|
|
79
102
|
refresh,
|
|
80
103
|
requestPermission
|
|
81
104
|
};
|
|
@@ -48,16 +48,19 @@ function useTextareaTriggerMenu(ref, options) {
|
|
|
48
48
|
function onKeyDown(event) {
|
|
49
49
|
handleKeyDown(event);
|
|
50
50
|
}
|
|
51
|
+
function syncAfterInput() {
|
|
52
|
+
window.setTimeout(syncTrigger, 0);
|
|
53
|
+
}
|
|
51
54
|
function onBlur() {
|
|
52
55
|
window.setTimeout(closeAfterBlur, 120);
|
|
53
56
|
}
|
|
54
|
-
element.addEventListener("input",
|
|
57
|
+
element.addEventListener("input", syncAfterInput);
|
|
55
58
|
element.addEventListener("keydown", onKeyDown);
|
|
56
59
|
element.addEventListener("keyup", syncTrigger);
|
|
57
60
|
element.addEventListener("click", syncTrigger);
|
|
58
61
|
element.addEventListener("blur", onBlur);
|
|
59
62
|
return () => {
|
|
60
|
-
element.removeEventListener("input",
|
|
63
|
+
element.removeEventListener("input", syncAfterInput);
|
|
61
64
|
element.removeEventListener("keydown", onKeyDown);
|
|
62
65
|
element.removeEventListener("keyup", syncTrigger);
|
|
63
66
|
element.removeEventListener("click", syncTrigger);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
//#region src/knob-contracts/audio-device-select-knobs.d.ts
|
|
2
|
+
export declare const audioDeviceSelectKnobs: readonly ["--cui-audio-device-select-status-foreground"];
|
|
3
|
+
export type AudioDeviceSelectKnobStyle = Partial<Record<(typeof audioDeviceSelectKnobs)[number], string>>;
|
|
4
|
+
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
//#region src/knob-contracts/audio-visualizer-knobs.d.ts
|
|
2
|
-
export declare const audioVisualizerKnobs: readonly ["--cui-audio-visualizer-radius", "--cui-audio-visualizer-bar-radius", "--cui-audio-visualizer-bar-background", "--cui-audio-visualizer-line-fill", "--cui-audio-visualizer-line-stroke", "--cui-audio-visualizer-baseline-stroke"];
|
|
2
|
+
export declare const audioVisualizerKnobs: readonly ["--cui-audio-visualizer-bar-size", "--cui-audio-visualizer-bar-gap", "--cui-audio-visualizer-radius", "--cui-audio-visualizer-bar-radius", "--cui-audio-visualizer-bar-background", "--cui-audio-visualizer-line-fill", "--cui-audio-visualizer-line-stroke", "--cui-audio-visualizer-baseline-stroke"];
|
|
3
3
|
export type AudioVisualizerKnobStyle = Partial<Record<(typeof audioVisualizerKnobs)[number], string>>;
|
|
4
4
|
//#endregion
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/knob-contracts/audio-visualizer-knobs.ts
|
|
2
2
|
const audioVisualizerKnobs = [
|
|
3
|
+
"--cui-audio-visualizer-bar-size",
|
|
4
|
+
"--cui-audio-visualizer-bar-gap",
|
|
3
5
|
"--cui-audio-visualizer-radius",
|
|
4
6
|
"--cui-audio-visualizer-bar-radius",
|
|
5
7
|
"--cui-audio-visualizer-bar-background",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/lib/audio-visualizer-levels.d.ts
|
|
2
|
+
export declare function audioVisualizerPointCount(points?: number): number;
|
|
3
|
+
export declare function normalizeAudioLevel(level: number): number;
|
|
4
|
+
export declare function audioVisualizerHistory(levels: ArrayLike<number>, points?: number, active?: boolean): {
|
|
5
|
+
key: string;
|
|
6
|
+
level: number;
|
|
7
|
+
}[];
|
|
8
|
+
export declare function audioVisualizerBands(levels: ArrayLike<number>, points?: number, active?: boolean, mirrored?: boolean): {
|
|
9
|
+
key: string;
|
|
10
|
+
level: number;
|
|
11
|
+
}[];
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/lib/audio-visualizer-levels.ts
|
|
2
|
+
function audioVisualizerPointCount(points = 28) {
|
|
3
|
+
return Math.min(128, Math.max(1, Number.isFinite(points) ? Math.floor(points) : 28));
|
|
4
|
+
}
|
|
5
|
+
function normalizeAudioLevel(level) {
|
|
6
|
+
return Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0;
|
|
7
|
+
}
|
|
8
|
+
function audioVisualizerHistory(levels, points, active = true) {
|
|
9
|
+
const count = audioVisualizerPointCount(points);
|
|
10
|
+
const start = Math.max(0, levels.length - count);
|
|
11
|
+
const padding = Math.max(0, count - levels.length);
|
|
12
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
13
|
+
key: `bar-${index}`,
|
|
14
|
+
level: active && index >= padding ? normalizeAudioLevel(levels[start + index - padding] ?? 0) : 0
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
function audioVisualizerBands(levels, points, active = true, mirrored = false) {
|
|
18
|
+
const count = audioVisualizerPointCount(points);
|
|
19
|
+
const bandCount = mirrored ? Math.ceil(count / 2) : count;
|
|
20
|
+
const bands = Array.from({ length: bandCount }, (_, index) => {
|
|
21
|
+
if (!active || levels.length === 0) return 0;
|
|
22
|
+
const start = index * levels.length / bandCount;
|
|
23
|
+
const end = (index + 1) * levels.length / bandCount;
|
|
24
|
+
let total = 0;
|
|
25
|
+
for (let sample = Math.floor(start); sample < Math.ceil(end); sample += 1) {
|
|
26
|
+
const weight = Math.min(end, sample + 1) - Math.max(start, sample);
|
|
27
|
+
total += normalizeAudioLevel(levels[sample] ?? 0) * weight;
|
|
28
|
+
}
|
|
29
|
+
return total / (end - start);
|
|
30
|
+
});
|
|
31
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
32
|
+
key: `band-${index}`,
|
|
33
|
+
level: bands[mirrored ? Math.floor(Math.abs(index - (count - 1) / 2)) : index] ?? 0
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { audioVisualizerBands, audioVisualizerHistory, audioVisualizerPointCount, normalizeAudioLevel };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SelectTriggerProps, SelectTriggerVariant } from "./ui/select.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/model-switcher.d.ts
|
|
4
|
+
export type ModelOption = {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
export type ModelSwitcherProps = {
|
|
11
|
+
models: ModelOption[];
|
|
12
|
+
value?: string;
|
|
13
|
+
defaultValue?: string;
|
|
14
|
+
onValueChange?: (value: string) => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
size?: "xs" | "sm";
|
|
17
|
+
variant?: SelectTriggerVariant;
|
|
18
|
+
className?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
style?: SelectTriggerProps["style"];
|
|
21
|
+
};
|
|
22
|
+
export declare function ModelSwitcher({ models, value, defaultValue, onValueChange, disabled, size, variant, className, label, style }: ModelSwitcherProps): import("react").JSX.Element;
|
|
23
|
+
//#endregion
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "./lib/cn.js";
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/model-switcher.tsx
|
|
6
|
+
function ModelSwitcher({ models, value, defaultValue, onValueChange, disabled, size = "sm", variant = "surface", className, label = "Model", style }) {
|
|
7
|
+
const fallback = defaultValue ?? models[0]?.value;
|
|
8
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
9
|
+
value,
|
|
10
|
+
defaultValue: fallback,
|
|
11
|
+
onValueChange,
|
|
12
|
+
disabled: disabled || models.length === 0,
|
|
13
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
14
|
+
size,
|
|
15
|
+
variant,
|
|
16
|
+
className: cn("max-w-52", className),
|
|
17
|
+
style,
|
|
18
|
+
"aria-label": label,
|
|
19
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
20
|
+
"data-control-ui": "model-switcher",
|
|
21
|
+
"data-control-family": "button",
|
|
22
|
+
"data-button-kind": "model-switcher",
|
|
23
|
+
"data-slot": "value",
|
|
24
|
+
className: "flex min-w-0 items-center",
|
|
25
|
+
children: /* @__PURE__ */ jsx(SelectValue, {
|
|
26
|
+
placeholder: label,
|
|
27
|
+
children: (current) => {
|
|
28
|
+
const model = models.find((option) => option.value === current);
|
|
29
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
30
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
31
|
+
children: [model?.icon ? /* @__PURE__ */ jsx("span", {
|
|
32
|
+
"aria-hidden": "true",
|
|
33
|
+
className: "flex shrink-0 [&_svg]:size-4",
|
|
34
|
+
children: model.icon
|
|
35
|
+
}) : null, /* @__PURE__ */ jsx("span", {
|
|
36
|
+
className: "truncate",
|
|
37
|
+
title: model?.label ?? label,
|
|
38
|
+
children: model?.label ?? label
|
|
39
|
+
})]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: models.map((model) => /* @__PURE__ */ jsxs(SelectItem, {
|
|
45
|
+
value: model.value,
|
|
46
|
+
label: model.label,
|
|
47
|
+
children: [
|
|
48
|
+
model.icon ? /* @__PURE__ */ jsx("span", {
|
|
49
|
+
"aria-hidden": "true",
|
|
50
|
+
className: "flex shrink-0 [&_svg]:size-4",
|
|
51
|
+
children: model.icon
|
|
52
|
+
}) : null,
|
|
53
|
+
/* @__PURE__ */ jsx("span", {
|
|
54
|
+
className: "truncate",
|
|
55
|
+
title: model.label,
|
|
56
|
+
children: model.label
|
|
57
|
+
}),
|
|
58
|
+
model.hint ? /* @__PURE__ */ jsx("span", {
|
|
59
|
+
"data-control-ui": "model-switcher",
|
|
60
|
+
"data-control-family": "popup",
|
|
61
|
+
"data-popup-kind": "model-switcher",
|
|
62
|
+
"data-slot": "hint",
|
|
63
|
+
children: model.hint
|
|
64
|
+
}) : null
|
|
65
|
+
]
|
|
66
|
+
}, model.value)) })]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { ModelSwitcher };
|
package/dist/styles/index.css
CHANGED
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
@import "./recipes/sidebar-menu.css";
|
|
20
20
|
@import "./recipes/sidebar-parts.css";
|
|
21
21
|
@import "./recipes/app-shell-header.css";
|
|
22
|
-
@import "./recipes/audio-visualizer.css";
|
|
23
|
-
@import "./recipes/audio-recorder.css";
|
|
24
22
|
@import "./recipes/field.css";
|
|
25
23
|
@import "./recipes/field-parts.css";
|
|
24
|
+
@import "./recipes/audio-device-select.css";
|
|
25
|
+
@import "./recipes/audio-visualizer.css";
|
|
26
|
+
@import "./recipes/audio-recorder.css";
|
|
26
27
|
@import "./recipes/avatar.css";
|
|
27
28
|
@import "./recipes/badge.css";
|
|
28
29
|
@import "./recipes/breadcrumb.css";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
:where([data-control-family="audio-device-select"][data-slot="root"]) {
|
|
3
|
+
--cui-audio-device-select-status-foreground: var(--muted-foreground);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
:where([data-control-family="audio-device-select"][data-slot="status"]) {
|
|
7
|
+
color: var(--cui-audio-device-select-status-foreground);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@property --cui-audio-device-select-status-foreground {
|
|
12
|
+
syntax: "<color>";
|
|
13
|
+
inherits: true;
|
|
14
|
+
initial-value: transparent;
|
|
15
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
--cui-audio-visualizer-radius: var(--radius-control);
|
|
4
4
|
--cui-audio-visualizer-bar-background: oklch(from var(--primary) l c h / 0.8);
|
|
5
5
|
--cui-audio-visualizer-bar-radius: var(--radius-control);
|
|
6
|
+
--cui-audio-visualizer-bar-size: calc(var(--spacing) * 1);
|
|
7
|
+
--cui-audio-visualizer-bar-gap: 1px;
|
|
6
8
|
--cui-audio-visualizer-line-fill: oklch(from var(--primary) l c h / 0.18);
|
|
7
9
|
--cui-audio-visualizer-line-stroke: oklch(from var(--primary) l c h / 0.8);
|
|
8
10
|
--cui-audio-visualizer-baseline-stroke: oklch(from var(--border) l c h / 0.55);
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
:where([data-control-family="audio-visualizer"][data-slot="track"]) {
|
|
27
|
-
gap:
|
|
29
|
+
gap: var(--cui-audio-visualizer-bar-gap);
|
|
28
30
|
opacity: 0.55;
|
|
29
31
|
transition: opacity var(--duration-base) var(--ease-standard);
|
|
30
32
|
}
|
|
@@ -34,7 +36,9 @@
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
:where([data-control-family="audio-visualizer"][data-slot="bar-track"]) {
|
|
37
|
-
inline-size:
|
|
39
|
+
inline-size: var(--cui-audio-visualizer-bar-size);
|
|
40
|
+
min-inline-size: 0;
|
|
41
|
+
flex-shrink: 1;
|
|
38
42
|
transition:
|
|
39
43
|
opacity var(--duration-base) var(--ease-emphasized),
|
|
40
44
|
filter var(--duration-base) var(--ease-emphasized),
|
|
@@ -69,6 +73,97 @@
|
|
|
69
73
|
stroke: var(--cui-audio-visualizer-line-stroke);
|
|
70
74
|
transition: d var(--duration-base) var(--ease-standard);
|
|
71
75
|
}
|
|
76
|
+
|
|
77
|
+
:where([data-control-family="audio-visualizer"][data-variant="bars"][data-active="true"] [data-slot="bar"]) {
|
|
78
|
+
transition: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:where([data-control-family="audio-visualizer"][data-variant="line"]:not([data-active="true"]) [data-slot="waveform"]) {
|
|
82
|
+
opacity: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:where([data-control-family="audio-visualizer"][data-variant="line"]:not([data-active="true"]) [data-slot="baseline"]) {
|
|
86
|
+
stroke-dasharray: 1 4;
|
|
87
|
+
stroke-linecap: round;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:where([data-control-family="audio-visualizer"][data-slot="root"][data-variant="bar"]) {
|
|
91
|
+
--cui-audio-visualizer-bar-size: calc(var(--spacing) * 1.5);
|
|
92
|
+
--cui-audio-visualizer-bar-gap: calc(var(--spacing) * 0.75);
|
|
93
|
+
padding-block: calc(var(--spacing) * 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"] [data-slot="track"]) {
|
|
97
|
+
display: flex;
|
|
98
|
+
block-size: 100%;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-align="start"] [data-slot="track"]) {
|
|
104
|
+
align-items: flex-start;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-align="end"] [data-slot="track"]) {
|
|
108
|
+
align-items: flex-end;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"] [data-slot="bar"]) {
|
|
112
|
+
min-inline-size: 0;
|
|
113
|
+
max-inline-size: var(--cui-audio-visualizer-bar-size);
|
|
114
|
+
flex: 1;
|
|
115
|
+
transform-origin: center;
|
|
116
|
+
transition:
|
|
117
|
+
height var(--duration-fast) var(--ease-standard),
|
|
118
|
+
opacity var(--duration-fast) var(--ease-standard);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-align="start"] [data-slot="bar"]) {
|
|
122
|
+
transform-origin: top;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-align="end"] [data-slot="bar"]) {
|
|
126
|
+
transform-origin: bottom;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-idle="pulse"]:not([data-active="true"]) [data-slot="bar"]) {
|
|
130
|
+
animation: cui-audio-bar-pulse calc(var(--duration-slow) * 4) var(--ease-standard) infinite;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-idle="wave"]:not([data-active="true"]) [data-slot="bar"]),
|
|
134
|
+
:where([data-control-family="audio-visualizer"][data-variant="bar"][data-loading="true"] [data-slot="bar"]) {
|
|
135
|
+
animation: cui-audio-bar-pulse calc(var(--duration-slow) * 4) var(--ease-standard) infinite;
|
|
136
|
+
animation-delay: var(--_audio-visualizer-delay);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes cui-audio-bar-pulse {
|
|
140
|
+
0%,
|
|
141
|
+
100% {
|
|
142
|
+
transform: scaleY(1);
|
|
143
|
+
}
|
|
144
|
+
50% {
|
|
145
|
+
transform: scaleY(4);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (prefers-reduced-motion: reduce) {
|
|
150
|
+
:where([data-control-family="audio-visualizer"] [data-slot]) {
|
|
151
|
+
animation: none;
|
|
152
|
+
transition: none;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@property --cui-audio-visualizer-bar-size {
|
|
158
|
+
syntax: "<length>";
|
|
159
|
+
inherits: true;
|
|
160
|
+
initial-value: 4px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@property --cui-audio-visualizer-bar-gap {
|
|
164
|
+
syntax: "<length>";
|
|
165
|
+
inherits: true;
|
|
166
|
+
initial-value: 1px;
|
|
72
167
|
}
|
|
73
168
|
|
|
74
169
|
@property --cui-audio-visualizer-radius {
|
package/dist/styles/theme.css
CHANGED
|
@@ -638,6 +638,14 @@
|
|
|
638
638
|
-moz-osx-font-smoothing: grayscale;
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
html {
|
|
642
|
+
background-color: var(--background);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
:root:has([data-control-family="sidebar"][data-slot="wrapper"] [data-control-family="sidebar"][data-variant="inset"][data-state]) {
|
|
646
|
+
background-color: var(--sidebar);
|
|
647
|
+
}
|
|
648
|
+
|
|
641
649
|
:where(
|
|
642
650
|
[data-control-family]:is(
|
|
643
651
|
a[href],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OpenChangeEventDetails } from "../control-props.js";
|
|
2
2
|
import { ControlSize } from "../control-variants.js";
|
|
3
|
-
import { PopupKnobStyle } from "../knob-contracts/popup-knobs.js";
|
|
4
3
|
import { FieldKnobStyle } from "../knob-contracts/field-knobs.js";
|
|
4
|
+
import { PopupKnobStyle } from "../knob-contracts/popup-knobs.js";
|
|
5
5
|
import { CSSProperties, ComponentProps, ReactNode } from "react";
|
|
6
6
|
//#region src/ui/autocomplete.d.ts
|
|
7
7
|
export type AutocompleteProps<Value = string> = {
|
package/dist/ui/color-picker.js
CHANGED
|
@@ -3,12 +3,12 @@ import { cn } from "../lib/cn.js";
|
|
|
3
3
|
import { controlEffectsAttribute } from "../skin.js";
|
|
4
4
|
import { useSkin } from "../skin-provider.js";
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
6
7
|
import { Input } from "./input.js";
|
|
7
8
|
import { useColorArea } from "../hooks/use-color-area.js";
|
|
8
9
|
import { formatColor, getChannels, hsvaToRgba, parseColor, pointToHueSat, pointToSaturationValue, setChannel } from "../lib/color.js";
|
|
9
10
|
import { TARGET_RATIO, contrastOf, fixColorForContrast, formatContrastRatio, nextFixLevel, wcagLevels } from "../lib/contrast.js";
|
|
10
11
|
import { NumberField, NumberFieldGroup, NumberFieldInput } from "./number-field.js";
|
|
11
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
12
12
|
import { createContext, useContext, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
|
|
13
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
import { Popover } from "@base-ui/react/popover";
|
package/dist/ui/combobox.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ControlSize } from "../control-variants.js";
|
|
2
|
-
import { PopupKnobStyle } from "../knob-contracts/popup-knobs.js";
|
|
3
2
|
import { FieldKnobStyle } from "../knob-contracts/field-knobs.js";
|
|
3
|
+
import { PopupKnobStyle } from "../knob-contracts/popup-knobs.js";
|
|
4
4
|
import { CSSProperties, ComponentProps, ReactNode } from "react";
|
|
5
5
|
import { Combobox as Combobox$1 } from "@base-ui/react/combobox";
|
|
6
6
|
//#region src/ui/combobox.d.ts
|
package/dist/ui/context-menu.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { controlEffectsAttribute } from "../skin.js";
|
|
4
4
|
import { useSkin } from "../skin-provider.js";
|
|
5
|
-
import { popupItemStructureClasses } from "../surface-variants.js";
|
|
6
5
|
import { popupItemFocusProps } from "../lib/popup-item-focus.js";
|
|
6
|
+
import { popupItemStructureClasses } from "../surface-variants.js";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
9
9
|
import { ContextMenu as ContextMenu$1 } from "@base-ui/react/context-menu";
|
package/dist/ui/dropdown-menu.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { controlEffectsAttribute } from "../skin.js";
|
|
4
4
|
import { useSkin } from "../skin-provider.js";
|
|
5
|
-
import { popupItemStructureClasses } from "../surface-variants.js";
|
|
6
5
|
import { popupItemFocusProps } from "../lib/popup-item-focus.js";
|
|
6
|
+
import { popupItemStructureClasses } from "../surface-variants.js";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
9
9
|
import { Menu } from "@base-ui/react/menu";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
3
4
|
import { useColorArea } from "../hooks/use-color-area.js";
|
|
4
5
|
import { formatGradient } from "../lib/color.js";
|
|
5
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
6
6
|
import { ColorPicker, ColorPickerAlpha, ColorPickerArea, ColorPickerContent, ColorPickerFormatSelect, ColorPickerHue, ColorPickerInput, ColorPickerTrigger } from "./color-picker.js";
|
|
7
7
|
import { createContext, useContext, useRef, useState } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
package/dist/ui/select.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ControlledChoice } from "../control-props.js";
|
|
2
2
|
import { ControlSize } from "../control-variants.js";
|
|
3
3
|
import { ButtonKnobStyle } from "../knob-contracts/button-knobs.js";
|
|
4
|
-
import { PopupKnobStyle } from "../knob-contracts/popup-knobs.js";
|
|
5
4
|
import { FieldKnobStyle } from "../knob-contracts/field-knobs.js";
|
|
5
|
+
import { PopupKnobStyle } from "../knob-contracts/popup-knobs.js";
|
|
6
6
|
import { CSSProperties, ComponentProps, ReactNode } from "react";
|
|
7
7
|
import { Select as Select$1 } from "@base-ui/react/select";
|
|
8
8
|
//#region src/ui/select.d.ts
|
package/dist/ui/select.js
CHANGED
|
@@ -4,8 +4,8 @@ import { controlEffectsAttribute } from "../skin.js";
|
|
|
4
4
|
import { useSkin } from "../skin-provider.js";
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
6
|
import { ScrollArea } from "./scroll-area.js";
|
|
7
|
-
import { popupItemStructureClasses } from "../surface-variants.js";
|
|
8
7
|
import { popupItemFocusProps } from "../lib/popup-item-focus.js";
|
|
8
|
+
import { popupItemStructureClasses } from "../surface-variants.js";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
11
11
|
import { Select as Select$1 } from "@base-ui/react/select";
|
|
@@ -47,7 +47,7 @@ function SidebarResizeRail({ className, ref, onPointerDown, onClick, onKeyDown,
|
|
|
47
47
|
const pointerId = event.pointerId;
|
|
48
48
|
const startX = event.clientX;
|
|
49
49
|
const anchoredLeft = anchoredToLeftEdge(handle);
|
|
50
|
-
const
|
|
50
|
+
const sidebarBounds = (handle.closest("[data-control-family=\"sidebar\"][data-slot=\"root\"]") ?? wrapper).getBoundingClientRect();
|
|
51
51
|
const initialWidth = measure.getBoundingClientRect().width;
|
|
52
52
|
const previousWidth = wrapper.style.getPropertyValue("--sidebar-width");
|
|
53
53
|
const previousCursor = document.body.style.cursor;
|
|
@@ -92,7 +92,7 @@ function SidebarResizeRail({ className, ref, onPointerDown, onClick, onKeyDown,
|
|
|
92
92
|
document.body.style.userSelect = "none";
|
|
93
93
|
wrapper.setAttribute("data-resizing", "true");
|
|
94
94
|
handle.setAttribute("data-resizing", "true");
|
|
95
|
-
const cursorWidth = anchoredLeft ? pointer.clientX -
|
|
95
|
+
const cursorWidth = anchoredLeft ? pointer.clientX - sidebarBounds.left : sidebarBounds.right - pointer.clientX;
|
|
96
96
|
if (updateCollapse(cursorWidth)) return;
|
|
97
97
|
draftWidth = clampWidth(cursorWidth);
|
|
98
98
|
wrapper.style.setProperty("--sidebar-width", `${draftWidth}px`);
|