@ctrl-ui/react 0.8.14 → 0.8.15

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.
Files changed (33) hide show
  1. package/dist/audio-device-select.d.ts +38 -0
  2. package/dist/audio-device-select.js +183 -0
  3. package/dist/audio-visualizer-bar.d.ts +10 -0
  4. package/dist/audio-visualizer-bar.js +49 -0
  5. package/dist/audio-visualizer-line.d.ts +4 -0
  6. package/dist/audio-visualizer-line.js +75 -0
  7. package/dist/audio-visualizer.d.ts +1 -1
  8. package/dist/audio-visualizer.js +43 -16
  9. package/dist/hooks/use-audio-recorder.d.ts +1 -0
  10. package/dist/hooks/use-audio-recorder.js +27 -4
  11. package/dist/hooks/use-textarea-trigger-menu.js +5 -2
  12. package/dist/knob-contracts/audio-device-select-knobs.d.ts +4 -0
  13. package/dist/knob-contracts/audio-device-select-knobs.js +4 -0
  14. package/dist/knob-contracts/audio-visualizer-knobs.d.ts +1 -1
  15. package/dist/knob-contracts/audio-visualizer-knobs.js +2 -0
  16. package/dist/lib/audio-visualizer-levels.d.ts +12 -0
  17. package/dist/lib/audio-visualizer-levels.js +37 -0
  18. package/dist/model-switcher.d.ts +23 -0
  19. package/dist/model-switcher.js +70 -0
  20. package/dist/styles/index.css +3 -2
  21. package/dist/styles/recipes/audio-device-select.css +15 -0
  22. package/dist/styles/recipes/audio-visualizer.css +97 -2
  23. package/dist/styles/theme.css +12 -0
  24. package/dist/ui/autocomplete.d.ts +1 -1
  25. package/dist/ui/color-picker.js +1 -1
  26. package/dist/ui/combobox.d.ts +1 -1
  27. package/dist/ui/context-menu.js +1 -1
  28. package/dist/ui/dropdown-menu.js +1 -1
  29. package/dist/ui/gradient-editor.js +1 -1
  30. package/dist/ui/select.d.ts +1 -1
  31. package/dist/ui/select.js +1 -1
  32. package/dist/ui/sidebar-resize-rail.js +2 -2
  33. 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,4 @@
1
+ import { AudioVisualizerProps } from "./audio-visualizer.js";
2
+ //#region src/audio-visualizer-line.d.ts
3
+ export declare function AudioVisualizer({ levels, points, active, className, style, ...props }: AudioVisualizerProps): import("react").JSX.Element;
4
+ //#endregion
@@ -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: readonly number[];
5
+ levels: ArrayLike<number>;
6
6
  active?: boolean;
7
7
  points?: number;
8
8
  style?: CSSProperties & AudioVisualizerKnobStyle;
@@ -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 DEFAULT_POINTS = 28;
5
- const MAX_POINTS = 128;
6
- const MIN_VISIBLE_LEVEL = .12;
7
- function resolveAudioVisualizerLevels(levels, points) {
8
- const pointCount = Math.min(MAX_POINTS, Math.max(1, Number.isFinite(points) ? Math.floor(points ?? DEFAULT_POINTS) : DEFAULT_POINTS));
9
- const visible = levels.slice(-pointCount).map((level) => Number.isFinite(level) ? Math.min(1, Math.max(0, level)) : 0);
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 = resolveAudioVisualizerLevels(levels, points);
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
- "aria-hidden": "true",
24
- className: cn("shrink-0 overflow-hidden", className),
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-end mask-l-from-90% rtl:mask-l-from-100% rtl:mask-r-from-90%",
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 * .88;
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 shrink-0 items-center",
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",
@@ -37,6 +37,7 @@ export type UseAudioInputDevicesResult = {
37
37
  loading: boolean;
38
38
  error: Error | null;
39
39
  hasPermission: boolean;
40
+ permission: "granted" | "prompt" | "denied";
40
41
  refresh: () => Promise<void>;
41
42
  requestPermission: () => Promise<void>;
42
43
  };
@@ -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 [hasPermission, setHasPermission] = useState(false);
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)) setHasPermission(true);
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
- setHasPermission(true);
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", syncTrigger);
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", syncTrigger);
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
@@ -0,0 +1,4 @@
1
+ //#region src/knob-contracts/audio-device-select-knobs.ts
2
+ const audioDeviceSelectKnobs = ["--cui-audio-device-select-status-foreground"];
3
+ //#endregion
4
+ export { audioDeviceSelectKnobs };
@@ -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 };
@@ -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: 1px;
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: calc(var(--spacing) * 1);
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 {
@@ -638,6 +638,18 @@
638
638
  -moz-osx-font-smoothing: grayscale;
639
639
  }
640
640
 
641
+ html {
642
+ background-color: var(--background);
643
+ }
644
+
645
+ :root:has([data-app-shell]:not([data-scroll="page"])) {
646
+ overscroll-behavior: none;
647
+ }
648
+
649
+ :root:has([data-control-family="sidebar"][data-slot="wrapper"] [data-control-family="sidebar"][data-variant="inset"][data-state]) {
650
+ background-color: var(--sidebar);
651
+ }
652
+
641
653
  :where(
642
654
  [data-control-family]:is(
643
655
  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> = {
@@ -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";
@@ -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
@@ -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";
@@ -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";
@@ -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 wrapperBounds = wrapper.getBoundingClientRect();
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 - wrapperBounds.left : wrapperBounds.right - 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`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl-ui/react",
3
- "version": "0.8.14",
3
+ "version": "0.8.15",
4
4
  "description": "Control UI components, blocks, and primitives with neutral defaults and consumer-owned themes.",
5
5
  "license": "MIT",
6
6
  "repository": {