@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.
Files changed (46) hide show
  1. package/dist/audio-player/index.cjs +2099 -0
  2. package/dist/audio-player/index.cjs.map +1 -0
  3. package/dist/audio-player/index.css +65 -0
  4. package/dist/audio-player/index.css.map +1 -0
  5. package/dist/audio-player/index.d.cts +174 -0
  6. package/dist/audio-player/index.d.ts +174 -0
  7. package/dist/audio-player/index.mjs +2076 -0
  8. package/dist/audio-player/index.mjs.map +1 -0
  9. package/dist/composer-registry/index.cjs +45 -0
  10. package/dist/composer-registry/index.cjs.map +1 -0
  11. package/dist/composer-registry/index.d.cts +73 -0
  12. package/dist/composer-registry/index.d.ts +73 -0
  13. package/dist/composer-registry/index.mjs +39 -0
  14. package/dist/composer-registry/index.mjs.map +1 -0
  15. package/dist/tree/index.cjs +85 -63
  16. package/dist/tree/index.cjs.map +1 -1
  17. package/dist/tree/index.d.cts +15 -1
  18. package/dist/tree/index.d.ts +15 -1
  19. package/dist/tree/index.mjs +86 -64
  20. package/dist/tree/index.mjs.map +1 -1
  21. package/package.json +14 -9
  22. package/src/tools/chat/composer/Composer.tsx +8 -8
  23. package/src/tools/chat/context/ChatProvider.tsx +13 -78
  24. package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
  25. package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
  26. package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
  27. package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
  28. package/src/tools/data/Tree/TreeRoot.tsx +33 -109
  29. package/src/tools/data/Tree/components/TreeRow.tsx +11 -0
  30. package/src/tools/data/Tree/context/TreeContext.tsx +22 -3
  31. package/src/tools/data/Tree/context/menu/index.ts +1 -0
  32. package/src/tools/data/Tree/context/menu/render.tsx +75 -0
  33. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +16 -2
  34. package/src/tools/data/Tree/index.tsx +1 -0
  35. package/src/tools/data/Tree/types/index.ts +1 -1
  36. package/src/tools/data/Tree/types/root-props.ts +16 -0
  37. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
  38. package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
  39. package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
  40. package/src/tools/integration/ComposerRegistry/index.ts +105 -0
  41. package/src/tools/media/AudioPlayer/Player.tsx +2 -0
  42. package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
  43. package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
  44. package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
  45. package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
  46. package/src/tools/media/AudioPlayer/types.ts +15 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import type { ButtonHTMLAttributes, ReactNode } from 'react';
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
- export function IconButton({
23
- label,
24
- shortcut,
25
- active,
26
- children,
27
- noTooltip,
28
- className = '',
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, useRef, useState } from 'react';
5
- import { useMediaQuery } from '@djangocfg/ui-core/hooks';
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
- // Hover bindings only apply on devices with real hover. On touch the popover
95
- // is toggled by the icon click.
96
- const hoverHandlers = isTouch
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
- <div ref={containerRef} className="relative" {...hoverHandlers}>
109
- <IconButton
110
- label={isOpen ? 'Close volume' : muted ? 'Unmute' : 'Volume'}
111
- shortcut={isTouch ? undefined : 'M'}
112
- active={muted}
113
- // Inside the popover wrapper disable IconButton's tooltip when the
114
- // popover is open (the popover already shows controls; nesting Radix
115
- // tooltip+popover content fights for focus/dismiss).
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
- <div className="flex items-center gap-2 rounded-md border border-border/60 bg-card px-3 py-2 shadow-sm">
135
- <input
136
- type="range"
137
- min={0}
138
- max={1}
139
- step={0.01}
140
- value={muted ? 0 : volume}
141
- onChange={(e) => {
142
- const v = parseFloat(e.target.value);
143
- setVolume(v);
144
- setVol(v);
145
- if (v > 0) setMuted(false);
146
- }}
147
- className="h-1 w-32 appearance-none rounded-full bg-muted accent-foreground"
148
- aria-label="Volume"
149
- />
150
- <span className="w-8 tabular-nums text-right text-[10px] text-muted-foreground">
151
- {Math.round((muted ? 0 : volume) * 100)}
152
- </span>
153
- {isTouch && (
154
- <button
155
- type="button"
156
- aria-label={muted ? 'Unmute' : 'Mute'}
157
- className="audioplayer-press grid h-7 w-7 place-items-center rounded text-muted-foreground hover:bg-accent"
158
- onClick={() => {
159
- toggleMute();
160
- setMuted(audio.muted);
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
  };