@djangocfg/ui-tools 2.1.416 → 2.1.417
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-player/index.cjs +2099 -0
- package/dist/audio-player/index.cjs.map +1 -0
- package/dist/audio-player/index.css +65 -0
- package/dist/audio-player/index.css.map +1 -0
- package/dist/audio-player/index.d.cts +174 -0
- package/dist/audio-player/index.d.ts +174 -0
- package/dist/audio-player/index.mjs +2076 -0
- package/dist/audio-player/index.mjs.map +1 -0
- package/dist/composer-registry/index.cjs +45 -0
- package/dist/composer-registry/index.cjs.map +1 -0
- package/dist/composer-registry/index.d.cts +73 -0
- package/dist/composer-registry/index.d.ts +73 -0
- package/dist/composer-registry/index.mjs +39 -0
- package/dist/composer-registry/index.mjs.map +1 -0
- package/dist/tree/index.cjs +85 -63
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +15 -1
- package/dist/tree/index.d.ts +15 -1
- package/dist/tree/index.mjs +86 -64
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +14 -9
- package/src/tools/chat/composer/Composer.tsx +8 -8
- package/src/tools/chat/context/ChatProvider.tsx +13 -78
- package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
- package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
- package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
- package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
- package/src/tools/data/Tree/TreeRoot.tsx +33 -109
- package/src/tools/data/Tree/components/TreeRow.tsx +11 -0
- package/src/tools/data/Tree/context/TreeContext.tsx +22 -3
- package/src/tools/data/Tree/context/menu/index.ts +1 -0
- package/src/tools/data/Tree/context/menu/render.tsx +75 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +16 -2
- package/src/tools/data/Tree/index.tsx +1 -0
- package/src/tools/data/Tree/types/index.ts +1 -1
- package/src/tools/data/Tree/types/root-props.ts +16 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
- package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
- package/src/tools/integration/ComposerRegistry/index.ts +105 -0
- package/src/tools/media/AudioPlayer/Player.tsx +2 -0
- package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
- package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
- package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
- package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
- package/src/tools/media/AudioPlayer/types.ts +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Tooltip,
|
|
6
6
|
TooltipContent,
|
|
@@ -19,21 +19,20 @@ type Props = {
|
|
|
19
19
|
noTooltip?: boolean;
|
|
20
20
|
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'aria-label'>;
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
children,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...rest
|
|
30
|
-
}: Props) {
|
|
22
|
+
// `forwardRef` is mandatory: this button is used as `<PopoverTrigger asChild>`
|
|
23
|
+
// in VolumeControl. Without ref forwarding, Radix can't anchor the popper to
|
|
24
|
+
// the trigger, and `<PopoverContent>` renders with zero rect (invisible).
|
|
25
|
+
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
|
|
26
|
+
{ label, shortcut, active, children, noTooltip, className = '', ...rest },
|
|
27
|
+
ref,
|
|
28
|
+
) {
|
|
31
29
|
// Active state uses a tinted primary surface — readable, on-brand, not loud.
|
|
32
30
|
const stateClasses = active
|
|
33
31
|
? 'bg-primary/10 text-primary hover:bg-primary/15'
|
|
34
32
|
: 'text-muted-foreground hover:bg-accent hover:text-foreground';
|
|
35
33
|
const button = (
|
|
36
34
|
<button
|
|
35
|
+
ref={ref}
|
|
37
36
|
type="button"
|
|
38
37
|
aria-label={label}
|
|
39
38
|
aria-pressed={active}
|
|
@@ -59,4 +58,4 @@ export function IconButton({
|
|
|
59
58
|
</TooltipContent>
|
|
60
59
|
</Tooltip>
|
|
61
60
|
);
|
|
62
|
-
}
|
|
61
|
+
});
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Volume2, VolumeX } from 'lucide-react';
|
|
4
|
-
import { useEffect,
|
|
5
|
-
import {
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger, Slider } from '@djangocfg/ui-core/components';
|
|
6
6
|
import { usePlayerAudio, usePlayerControls } from '../../context/selectors';
|
|
7
7
|
import { IconButton } from './IconButton';
|
|
8
8
|
|
|
9
|
-
const CLOSE_DELAY_MS = 120;
|
|
10
|
-
|
|
11
9
|
// `audio.volume` is read-only on iOS Safari (controlled by hardware buttons),
|
|
12
10
|
// so a JS slider does nothing useful there. Detect once at module load.
|
|
13
11
|
function isIosSafari(): boolean {
|
|
@@ -19,16 +17,22 @@ function isIosSafari(): boolean {
|
|
|
19
17
|
}
|
|
20
18
|
const HIDE_VOLUME = isIosSafari();
|
|
21
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Volume control — click-to-open Popover with a vertical `<Slider>`.
|
|
22
|
+
*
|
|
23
|
+
* Click on the trigger toggles the popover (not mute). Mute lives as a
|
|
24
|
+
* dedicated icon button inside the popover. This avoids the classic
|
|
25
|
+
* Radix `<Tooltip>` + `<Popover>` collision on the same trigger:
|
|
26
|
+
* hovering opened the tooltip, click toggled the popover — but the
|
|
27
|
+
* tooltip's pointer-events fought the popover's outside-click logic.
|
|
28
|
+
* One trigger, one job: open the popover; volume + mute live inside.
|
|
29
|
+
*/
|
|
22
30
|
export function VolumeControl() {
|
|
23
31
|
const audio = usePlayerAudio();
|
|
24
32
|
const { setVolume, toggleMute } = usePlayerControls();
|
|
25
33
|
const [volume, setVol] = useState(audio.volume);
|
|
26
34
|
const [muted, setMuted] = useState(audio.muted);
|
|
27
35
|
const [isOpen, setOpen] = useState(false);
|
|
28
|
-
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
|
-
|
|
30
|
-
// Touch devices have no hover — open the popover on click instead.
|
|
31
|
-
const isTouch = useMediaQuery('(hover: none), (pointer: coarse)');
|
|
32
36
|
|
|
33
37
|
useEffect(() => {
|
|
34
38
|
const sync = () => {
|
|
@@ -39,26 +43,7 @@ export function VolumeControl() {
|
|
|
39
43
|
return () => audio.removeEventListener('volumechange', sync);
|
|
40
44
|
}, [audio]);
|
|
41
45
|
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
return () => {
|
|
44
|
-
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
45
|
-
};
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
// Close on outside-click in touch mode.
|
|
49
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!isOpen || !isTouch) return;
|
|
52
|
-
const onDown = (e: PointerEvent) => {
|
|
53
|
-
if (!containerRef.current) return;
|
|
54
|
-
if (!containerRef.current.contains(e.target as Node)) setOpen(false);
|
|
55
|
-
};
|
|
56
|
-
document.addEventListener('pointerdown', onDown);
|
|
57
|
-
return () => document.removeEventListener('pointerdown', onDown);
|
|
58
|
-
}, [isOpen, isTouch]);
|
|
59
|
-
|
|
60
46
|
if (HIDE_VOLUME) {
|
|
61
|
-
// iOS Safari can't change volume via JS — keep mute toggle only.
|
|
62
47
|
return (
|
|
63
48
|
<IconButton
|
|
64
49
|
label={muted ? 'Unmute' : 'Mute'}
|
|
@@ -74,98 +59,50 @@ export function VolumeControl() {
|
|
|
74
59
|
);
|
|
75
60
|
}
|
|
76
61
|
|
|
77
|
-
const cancelClose = () => {
|
|
78
|
-
if (closeTimer.current) {
|
|
79
|
-
clearTimeout(closeTimer.current);
|
|
80
|
-
closeTimer.current = null;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const scheduleClose = () => {
|
|
84
|
-
cancelClose();
|
|
85
|
-
closeTimer.current = setTimeout(() => setOpen(false), CLOSE_DELAY_MS);
|
|
86
|
-
};
|
|
87
|
-
const open = () => {
|
|
88
|
-
cancelClose();
|
|
89
|
-
setOpen(true);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
62
|
const Icon = muted || volume === 0 ? VolumeX : Volume2;
|
|
93
63
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
onPointerEnter: open,
|
|
100
|
-
onPointerLeave: scheduleClose,
|
|
101
|
-
onFocusCapture: open,
|
|
102
|
-
onBlurCapture: (e: React.FocusEvent) => {
|
|
103
|
-
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) scheduleClose();
|
|
104
|
-
},
|
|
105
|
-
};
|
|
64
|
+
const handleChange = (v: number) => {
|
|
65
|
+
setVolume(v);
|
|
66
|
+
setVol(v);
|
|
67
|
+
if (v > 0) setMuted(false);
|
|
68
|
+
};
|
|
106
69
|
|
|
107
70
|
return (
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
noTooltip={isOpen}
|
|
117
|
-
onClick={() => {
|
|
118
|
-
if (isTouch) {
|
|
119
|
-
setOpen((v) => !v);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
toggleMute();
|
|
123
|
-
setMuted(audio.muted);
|
|
124
|
-
}}
|
|
125
|
-
>
|
|
126
|
-
<Icon size={16} strokeWidth={1.75} />
|
|
127
|
-
</IconButton>
|
|
128
|
-
{isOpen && (
|
|
129
|
-
<div
|
|
130
|
-
className="absolute bottom-full left-1/2 z-20 -translate-x-1/2 pb-2"
|
|
131
|
-
onPointerEnter={isTouch ? undefined : open}
|
|
132
|
-
onPointerLeave={isTouch ? undefined : scheduleClose}
|
|
71
|
+
<Popover open={isOpen} onOpenChange={setOpen}>
|
|
72
|
+
<PopoverTrigger asChild>
|
|
73
|
+
<IconButton
|
|
74
|
+
label={isOpen ? 'Close volume' : 'Volume'}
|
|
75
|
+
// Tooltip while open would race the popover for pointer/focus
|
|
76
|
+
// — suppress it as soon as the popover opens.
|
|
77
|
+
noTooltip={isOpen}
|
|
78
|
+
active={muted}
|
|
133
79
|
>
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}}
|
|
162
|
-
>
|
|
163
|
-
{muted ? <VolumeX size={14} strokeWidth={1.75} /> : <Volume2 size={14} strokeWidth={1.75} />}
|
|
164
|
-
</button>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
80
|
+
<Icon size={16} strokeWidth={1.75} />
|
|
81
|
+
</IconButton>
|
|
82
|
+
</PopoverTrigger>
|
|
83
|
+
<PopoverContent
|
|
84
|
+
side="top"
|
|
85
|
+
align="center"
|
|
86
|
+
sideOffset={6}
|
|
87
|
+
className="flex w-14! flex-col items-center gap-2 p-3!"
|
|
88
|
+
// Keep the focus ring on the trigger; the slider still accepts
|
|
89
|
+
// keyboard once tabbed into.
|
|
90
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
91
|
+
>
|
|
92
|
+
<span className="w-full text-center tabular-nums text-[10px] text-muted-foreground">
|
|
93
|
+
{Math.round((muted ? 0 : volume) * 100)}
|
|
94
|
+
</span>
|
|
95
|
+
<Slider
|
|
96
|
+
orientation="vertical"
|
|
97
|
+
min={0}
|
|
98
|
+
max={1}
|
|
99
|
+
step={0.01}
|
|
100
|
+
value={[muted ? 0 : volume]}
|
|
101
|
+
onValueChange={([v]) => handleChange(v)}
|
|
102
|
+
aria-label="Volume"
|
|
103
|
+
className="h-32"
|
|
104
|
+
/>
|
|
105
|
+
</PopoverContent>
|
|
106
|
+
</Popover>
|
|
170
107
|
);
|
|
171
108
|
}
|
|
@@ -51,6 +51,18 @@ export type PlayerProps = {
|
|
|
51
51
|
|
|
52
52
|
ariaLabel?: string;
|
|
53
53
|
enableKeyboardShortcuts?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Move keyboard focus into the player container on mount. Activates
|
|
56
|
+
* the hotkey scope (Space=play/pause, ←→=seek, ↑↓=volume, M=mute)
|
|
57
|
+
* without the user having to click the player first.
|
|
58
|
+
*
|
|
59
|
+
* Useful when the player mounts as the result of an explicit user
|
|
60
|
+
* action — e.g. a file picker selecting an audio file — so keyboard
|
|
61
|
+
* control is immediately live.
|
|
62
|
+
*
|
|
63
|
+
* @default false
|
|
64
|
+
*/
|
|
65
|
+
autoFocus?: boolean;
|
|
54
66
|
|
|
55
67
|
// When the user clicks on the waveform while paused, also start playback.
|
|
56
68
|
// Default true — clicking on a time mark almost always means "play here".
|
|
@@ -92,4 +104,7 @@ export type PlayerHandle = {
|
|
|
92
104
|
seek: (seconds: number) => void;
|
|
93
105
|
getCurrentTime: () => number;
|
|
94
106
|
getDuration: () => number;
|
|
107
|
+
/** Move keyboard focus to the player container so its hotkey scope
|
|
108
|
+
* (Space=play/pause, ←→=seek, ↑↓=volume, M=mute) becomes active. */
|
|
109
|
+
focus: () => void;
|
|
95
110
|
};
|