@djangocfg/ui-tools 2.1.415 → 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 (140) 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/file-icon/index.d.cts +1 -1
  16. package/dist/file-icon/index.d.ts +1 -1
  17. package/dist/slots-ClRpIzoh.d.cts +88 -0
  18. package/dist/slots-ClRpIzoh.d.ts +88 -0
  19. package/dist/tree/index.cjs +2019 -279
  20. package/dist/tree/index.cjs.map +1 -1
  21. package/dist/tree/index.d.cts +731 -72
  22. package/dist/tree/index.d.ts +731 -72
  23. package/dist/tree/index.mjs +2009 -282
  24. package/dist/tree/index.mjs.map +1 -1
  25. package/package.json +18 -9
  26. package/src/tools/chat/README.md +111 -1
  27. package/src/tools/chat/composer/Composer.tsx +146 -25
  28. package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
  29. package/src/tools/chat/composer/index.ts +22 -0
  30. package/src/tools/chat/composer/slash/README.md +187 -0
  31. package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
  32. package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
  33. package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
  34. package/src/tools/chat/composer/slash/index.ts +44 -0
  35. package/src/tools/chat/composer/slash/labels.ts +19 -0
  36. package/src/tools/chat/composer/slash/state.ts +168 -0
  37. package/src/tools/chat/composer/slash/types.ts +64 -0
  38. package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
  39. package/src/tools/chat/composer/types.ts +8 -0
  40. package/src/tools/chat/context/ChatProvider.tsx +13 -78
  41. package/src/tools/chat/hooks/useAutoFocusOnStreamEnd.ts +12 -15
  42. package/src/tools/chat/hooks/useFocusOnEmptyClick.ts +4 -5
  43. package/src/tools/chat/launcher/header/ChatHeader.tsx +14 -19
  44. package/src/tools/chat/launcher/header/ChatHeaderActionButton.tsx +8 -12
  45. package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
  46. package/src/tools/chat/shell/index.ts +6 -0
  47. package/src/tools/data/Listbox/lazy.tsx +1 -1
  48. package/src/tools/data/Masonry/lazy.tsx +1 -1
  49. package/src/tools/data/Timeline/lazy.tsx +1 -1
  50. package/src/tools/data/Tree/FinderTree.tsx +42 -0
  51. package/src/tools/data/Tree/README.md +337 -208
  52. package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
  53. package/src/tools/data/Tree/TreeRoot.tsx +111 -72
  54. package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
  55. package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
  56. package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
  57. package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
  58. package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
  59. package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
  60. package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
  61. package/src/tools/data/Tree/components/TreeRow.tsx +103 -8
  62. package/src/tools/data/Tree/components/index.ts +6 -0
  63. package/src/tools/data/Tree/context/TreeContext.tsx +223 -363
  64. package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
  65. package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
  66. package/src/tools/data/Tree/context/async-children/index.ts +8 -0
  67. package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
  68. package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
  69. package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
  70. package/src/tools/data/Tree/context/dnd/index.ts +8 -0
  71. package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
  72. package/src/tools/data/Tree/context/expansion/index.ts +4 -0
  73. package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
  74. package/src/tools/data/Tree/context/hooks.ts +68 -1
  75. package/src/tools/data/Tree/context/index.ts +3 -0
  76. package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
  77. package/src/tools/data/Tree/context/menu/index.ts +11 -0
  78. package/src/tools/data/Tree/context/menu/render.tsx +75 -0
  79. package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +141 -0
  80. package/src/tools/data/Tree/context/persist/index.ts +4 -0
  81. package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
  82. package/src/tools/data/Tree/context/rename/index.ts +4 -0
  83. package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
  84. package/src/tools/data/Tree/context/selection/index.ts +4 -0
  85. package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
  86. package/src/tools/data/Tree/context/state/index.ts +6 -0
  87. package/src/tools/data/Tree/context/state/initial.ts +41 -0
  88. package/src/tools/data/Tree/context/state/reducer.ts +76 -0
  89. package/src/tools/data/Tree/context/state/types.ts +46 -0
  90. package/src/tools/data/Tree/data/clipboard.ts +33 -0
  91. package/src/tools/data/Tree/data/dnd.ts +123 -0
  92. package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
  93. package/src/tools/data/Tree/data/index.ts +19 -0
  94. package/src/tools/data/Tree/data/renameUtils.ts +51 -0
  95. package/src/tools/data/Tree/data/selection.ts +157 -0
  96. package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
  97. package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
  98. package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
  99. package/src/tools/data/Tree/hooks/index.ts +23 -4
  100. package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
  101. package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
  102. package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
  103. package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
  104. package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
  105. package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
  106. package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
  107. package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
  108. package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
  109. package/src/tools/data/Tree/index.tsx +26 -2
  110. package/src/tools/data/Tree/types/activation.ts +30 -0
  111. package/src/tools/data/Tree/types/adapter.ts +70 -0
  112. package/src/tools/data/Tree/types/index.ts +27 -0
  113. package/src/tools/data/Tree/types/labels.ts +97 -0
  114. package/src/tools/data/Tree/types/loader.ts +9 -0
  115. package/src/tools/data/Tree/types/node.ts +38 -0
  116. package/src/tools/data/Tree/types/root-props.ts +158 -0
  117. package/src/tools/data/Tree/types/selection.ts +3 -0
  118. package/src/tools/data/Tree/types/slots.ts +64 -0
  119. package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +6 -9
  120. package/src/tools/dev/OpenapiViewer/components/DocsLayout/index.tsx +2 -4
  121. package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
  122. package/src/tools/forms/MarkdownEditor/index.ts +1 -0
  123. package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
  124. package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
  125. package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
  126. package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
  127. package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
  128. package/src/tools/forms/MarkdownEditor/styles.css +18 -0
  129. package/src/tools/input/SpeechRecognition/widgets/VoiceComposerSlot.tsx +11 -12
  130. package/src/tools/integration/ComposerRegistry/index.ts +105 -0
  131. package/src/tools/media/AudioPlayer/Player.tsx +2 -0
  132. package/src/tools/media/AudioPlayer/PlayerShell.tsx +37 -22
  133. package/src/tools/media/AudioPlayer/lazy.tsx +30 -42
  134. package/src/tools/media/AudioPlayer/parts/Controls/IconButton.tsx +10 -11
  135. package/src/tools/media/AudioPlayer/parts/Controls/VolumeControl.tsx +52 -115
  136. package/src/tools/media/AudioPlayer/types.ts +15 -0
  137. package/dist/types-j2vhn4Kv.d.cts +0 -241
  138. package/dist/types-j2vhn4Kv.d.ts +0 -241
  139. package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
  140. package/src/tools/data/Tree/types.ts +0 -217
@@ -3,31 +3,41 @@
3
3
  /**
4
4
  * `@djangocfg/ui-tools/audio-player` subpath entrypoint.
5
5
  *
6
- * `LazyPlayer` is exported as a direct (synchronous) alias of `Player`. We
7
- * intentionally avoid `React.lazy` + `import('./Player')` here: under bundlers
8
- * that pre-bundle subpath entries (Vite optimizeDeps in Next.js/Vite/SB), the
9
- * dynamic import creates a second chunk that re-instantiates the React
10
- * Contexts (AudioRefCtx/ControlsCtx/MetaCtx/StateCtx/LevelsCtx). The slot
11
- * components and selector hooks re-exported below would then read from a
12
- * different context instance than `<PlayerProvider>` writes to, which made
13
- * `usePlayerAudio` throw "must be used inside <PlayerProvider>".
6
+ * We deliberately keep this surface narrow:
14
7
  *
15
- * Heavy audio-decoding work (peaks, AudioContext) already happens lazily at
16
- * runtime via effects inside `PlayerProvider`/`PlayerShell` — there is no
17
- * benefit to splitting the React shell behind a second chunk.
8
+ * - `Player` / `LazyPlayer` the only React component you should
9
+ * import. Already wraps `<PlayerProvider>` + `<PlayerShell>`.
10
+ * - types
11
+ * - `PlayerProvider` + selector hooks — for the rare consumer that
12
+ * wants to render a fully custom layout. They must own the
13
+ * provider themselves.
14
+ * - cross-instance store helpers (active player, preferences).
15
+ *
16
+ * Slot components (Cover/Title/Waveform/Controls/Layout) and internal
17
+ * peak-cache helpers are NOT re-exported from this entrypoint. Earlier
18
+ * versions did re-export them, which produced a second module-graph
19
+ * entry into the `context/*` files under Vite's `optimizeDeps`
20
+ * pre-bundling — the slot files import the same selectors as
21
+ * `PlayerShell`, but via a different URL key, so `<PlayerProvider>`
22
+ * (one context instance) and `usePlayerAudio` inside a slot read
23
+ * (another context instance) ended up on different `createContext()`
24
+ * objects → "usePlayerAudio must be used inside <PlayerProvider>".
25
+ *
26
+ * If you need raw slots, import from a deeper path (e.g.
27
+ * `@djangocfg/ui-tools/src/tools/media/AudioPlayer/parts/Cover`) and
28
+ * accept that you must own a single `<PlayerProvider>` boundary.
18
29
  */
19
30
 
20
31
  // ============================================================================
21
- // Player component (synchronous; previously lazy — see note above)
32
+ // Player component (synchronous; previously lazy)
22
33
  // ============================================================================
23
34
 
24
35
  export { Player, Player as LazyPlayer } from './Player';
25
36
 
26
37
  // ============================================================================
27
- // Light surface — types, store, context, slot components, hooks
38
+ // Types
28
39
  // ============================================================================
29
40
 
30
- // Types
31
41
  export type {
32
42
  PlayerProps,
33
43
  PlayerState,
@@ -41,7 +51,10 @@ export type {
41
51
  PlayerErrorReason,
42
52
  } from './types';
43
53
 
54
+ // ============================================================================
44
55
  // Context provider + selector hooks (no UI)
56
+ // ============================================================================
57
+
45
58
  export {
46
59
  PlayerProvider,
47
60
  usePlayerAudio,
@@ -53,7 +66,10 @@ export {
53
66
  usePlayerState,
54
67
  } from './context';
55
68
 
69
+ // ============================================================================
56
70
  // Cross-instance store (active player, preferences)
71
+ // ============================================================================
72
+
57
73
  export {
58
74
  setActivePlayer,
59
75
  getActivePlayer,
@@ -66,37 +82,9 @@ export {
66
82
  type PlayerPreferences,
67
83
  } from './store';
68
84
 
69
- // Store-backed hooks
70
85
  export {
71
86
  useActivePlayer,
72
87
  useLastActivePlayer,
73
88
  useIsActivePlayer,
74
89
  } from './hooks/useActivePlayer';
75
90
  export { usePlayerPreferences } from './hooks/usePlayerPreferences';
76
-
77
- // Peak cache helpers
78
- export { clearPeaksCache, setPeaks } from './audio';
79
-
80
- // Slot components — presentational, read from PlayerContext. Safe to
81
- // re-export synchronously: they don't import the heavy Player tree
82
- // (audio decoding, layouts, shell) — only context selectors and types.
83
- export { Cover, CoverPlaceholder, ReactivePulse } from './parts/Cover';
84
- export { Title, Artist, TimeDisplay } from './parts/Meta';
85
- export {
86
- PlayButton,
87
- SkipButton,
88
- VolumeControl,
89
- LoopButton,
90
- ControlsRow,
91
- IconButton,
92
- } from './parts/Controls';
93
- export {
94
- Waveform,
95
- PeaksWaveform,
96
- LiveWaveform,
97
- BarsWaveform,
98
- ProgressBar,
99
- WaveformSkeleton,
100
- } from './parts/Waveform';
101
- export { ErrorState } from './parts/ErrorState';
102
- export { DefaultLayout, CompactLayout } from './parts/Layout';
@@ -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
  };
@@ -1,241 +0,0 @@
1
- import { CSSProperties, ReactNode } from 'react';
2
-
3
- type TreeDensity = 'compact' | 'cozy' | 'comfortable';
4
- type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
5
- type TreeRadius = 'none' | 'sm' | 'md';
6
- /**
7
- * Cosmetic configuration. Every field is optional; missing values fall
8
- * back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
9
- *
10
- * Customize the look without re-implementing slots.
11
- */
12
- interface TreeAppearance {
13
- /** Built-in size preset. Default: `'cozy'`. */
14
- density?: TreeDensity;
15
- /** Override row height in px (wins over density). */
16
- rowHeight?: number;
17
- /** Override icon + chevron size in px (wins over density). */
18
- iconSize?: number;
19
- /** Lucide stroke width for icon + chevron. Default: 1.5. */
20
- iconStrokeWidth?: number;
21
- /** Override label font size in px (wins over density). */
22
- fontSize?: number;
23
- /** Pixels between chevron / icon / label. Default depends on density. */
24
- gap?: number;
25
- /** Pixels between nesting levels. Default: 16. */
26
- indent?: number;
27
- /** Hover / selected highlight intensity. Default: `'default'`. */
28
- accent?: TreeAccentIntensity;
29
- /** Row corner radius. Default: `'sm'`. */
30
- radius?: TreeRadius;
31
- /** Indent-guide line opacity (0..1). Default: 0.4. */
32
- indentGuideOpacity?: number;
33
- /**
34
- * Show a 2px primary-tinted bar on the left of the selected row.
35
- * Mimics the VSCode active-tab indicator. Default: `true`.
36
- */
37
- showActiveIndicator?: boolean;
38
- }
39
- interface ResolvedAppearance {
40
- density: TreeDensity;
41
- rowHeight: number;
42
- iconSize: number;
43
- iconStrokeWidth: number;
44
- fontSize: number;
45
- gap: number;
46
- indent: number;
47
- accent: TreeAccentIntensity;
48
- radius: TreeRadius;
49
- indentGuideOpacity: number;
50
- showActiveIndicator: boolean;
51
- }
52
- declare const DEFAULT_TREE_APPEARANCE: ResolvedAppearance;
53
- /**
54
- * Merge a partial appearance with the default + density preset.
55
- *
56
- * Explicit numeric overrides (e.g. `rowHeight`) win over the density preset.
57
- */
58
- declare function resolveAppearance(input?: TreeAppearance,
59
- /** Outer `indent` prop (kept on TreeRoot for back-compat). */
60
- outerIndent?: number): ResolvedAppearance;
61
- /**
62
- * Build the `style` object that exposes the resolved appearance to any
63
- * descendant via CSS variables. Set on `<TreeRoot>`'s outer div.
64
- */
65
- declare function appearanceToStyle(a: ResolvedAppearance): CSSProperties;
66
-
67
- type TreeItemId = string;
68
- /** A single node in the consumer's tree data. Generic over your payload. */
69
- interface TreeNode<T = unknown> {
70
- id: TreeItemId;
71
- data: T;
72
- /** Inline children. Omit (and provide a `loadChildren`) for async loading. */
73
- children?: TreeNode<T>[];
74
- /**
75
- * Set to `true` to mark a node as a folder even when its `children` array
76
- * is empty (e.g. an unloaded async folder). Default: derived from
77
- * `Array.isArray(children)`.
78
- */
79
- isFolder?: boolean;
80
- /** Disable interaction. */
81
- disabled?: boolean;
82
- }
83
- interface TreeLabels {
84
- loading: string;
85
- empty: string;
86
- error: string;
87
- searchPlaceholder: string;
88
- searchMatches: (count: number) => string;
89
- ariaLabel: string;
90
- }
91
- declare const DEFAULT_TREE_LABELS: TreeLabels;
92
- type TreeSelectionMode = 'none' | 'single' | 'multiple';
93
- /**
94
- * How a node becomes "activated" (i.e. opened) on pointer interaction.
95
- *
96
- * - `'single-click'` (default): single click activates a leaf immediately;
97
- * double-click also activates. Folders always toggle on single click.
98
- * - `'double-click'`: single click only selects + focuses; double-click is
99
- * required to activate. Mirrors classic file-manager behaviour.
100
- * - `'single-click-preview'`: VSCode Explorer / Cursor behaviour. Single
101
- * click activates with `{ preview: true }` (consumer renders a preview
102
- * tab); double-click activates with `{ preview: false }` (pinned tab).
103
- *
104
- * Folders ignore this setting — they always toggle on single click and
105
- * never call `onActivate`.
106
- */
107
- type TreeActivationMode = 'single-click' | 'double-click' | 'single-click-preview';
108
- interface TreeActivateOptions {
109
- /**
110
- * `true` when the activation came from a single click in
111
- * `'single-click-preview'` mode. `false` for double-click and for
112
- * non-preview modes. Consumers typically map this to a
113
- * preview-tab vs pinned-tab distinction.
114
- */
115
- preview: boolean;
116
- }
117
- /**
118
- * Async loader: called the first time a folder is expanded with no inline
119
- * `children`. Result is cached; concurrent expansions are de-duplicated.
120
- */
121
- type TreeLoadChildren<T> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
122
- interface TreeRowRenderProps<T> {
123
- node: TreeNode<T>;
124
- level: number;
125
- isSelected: boolean;
126
- isExpanded: boolean;
127
- isFocused: boolean;
128
- isFolder: boolean;
129
- isLoading: boolean;
130
- isMatchingSearch: boolean;
131
- }
132
- type TreeRowSlot<T> = (props: TreeRowRenderProps<T>) => ReactNode;
133
- type TreeContextMenuSlot<T> = (props: TreeRowRenderProps<T>, trigger: ReactNode) => ReactNode;
134
- /**
135
- * Declarative context-menu item. Pass `'separator'` (string) in place of an
136
- * object to insert a `<ContextMenuSeparator />` between groups.
137
- *
138
- * For more advanced needs (submenus, checkbox items, custom JSX), drop down
139
- * to `renderContextMenu` instead.
140
- */
141
- interface TreeContextMenuAction<T> {
142
- /** Stable React key. */
143
- id: string;
144
- label: ReactNode;
145
- /** Lucide-style icon component. Rendered as `<icon className="size-4" />`. */
146
- icon?: React.ComponentType<{
147
- className?: string;
148
- }>;
149
- /** Right-aligned keyboard hint (e.g. `'⌘C'`, `'↵'`). Cosmetic. */
150
- shortcut?: ReactNode;
151
- /** Disable the item — still rendered, not selectable. */
152
- disabled?: boolean;
153
- /** Style as destructive (red). */
154
- destructive?: boolean;
155
- /** Click / Enter handler. Receives the row meta. */
156
- onSelect: (props: TreeRowRenderProps<T>) => void;
157
- }
158
- type TreeContextMenuItem<T> = TreeContextMenuAction<T> | 'separator';
159
- type TreeContextMenuActionsResolver<T> = (props: TreeRowRenderProps<T>) => TreeContextMenuItem<T>[] | null | undefined;
160
- interface TreeRootProps<T> {
161
- /** Root nodes. Top-level items are rendered directly (no synthetic root). */
162
- data: TreeNode<T>[];
163
- /** Returns the human-readable name for a node (used by search/type-ahead). */
164
- getItemName: (node: TreeNode<T>) => string;
165
- /** Async loader for folders without inline `children`. */
166
- loadChildren?: TreeLoadChildren<T>;
167
- /** Selection behaviour. Default: `'single'`. */
168
- selectionMode?: TreeSelectionMode;
169
- /** Pointer activation behaviour. Default: `'single-click'`. */
170
- activationMode?: TreeActivationMode;
171
- /** Initially expanded ids. */
172
- initialExpandedIds?: TreeItemId[];
173
- /** Initially selected ids. */
174
- initialSelectedIds?: TreeItemId[];
175
- /** Pixels of indent per nesting level. Default: 16. (Shortcut for `appearance.indent`.) */
176
- indent?: number;
177
- /** Cosmetic configuration: density, sizes, accent intensity, radius. */
178
- appearance?: TreeAppearance;
179
- /** Triggered when selection changes. */
180
- onSelectionChange?: (selectedIds: TreeItemId[]) => void;
181
- /** Triggered when expanded set changes. */
182
- onExpansionChange?: (expandedIds: TreeItemId[]) => void;
183
- /**
184
- * Triggered when a leaf is activated (Enter / dblclick / click depending
185
- * on `activationMode`). Folders never call this — they toggle instead.
186
- */
187
- onActivate?: (node: TreeNode<T>, opts: TreeActivateOptions) => void;
188
- /**
189
- * Optional predicate. Nodes returning `false` (and their descendants) are
190
- * not rendered and not searchable. Use this to hide dot-files, system
191
- * entries, or anything else the consumer wants to filter out.
192
- */
193
- filterNode?: (node: TreeNode<T>) => boolean;
194
- /** Show built-in search input. Default: false. */
195
- enableSearch?: boolean;
196
- /** Type printable letters to jump to a matching name. Default: true. */
197
- enableTypeAhead?: boolean;
198
- /** Render vertical indent guides under expanded folders. Default: false. */
199
- showIndentGuides?: boolean;
200
- /** Custom row renderer. Falls back to the default <TreeRow />. */
201
- renderRow?: TreeRowSlot<T>;
202
- /** Replace default folder/file icon. */
203
- renderIcon?: TreeRowSlot<T>;
204
- /** Replace default label rendering. */
205
- renderLabel?: TreeRowSlot<T>;
206
- /** Right-side actions slot (per row). */
207
- renderActions?: TreeRowSlot<T>;
208
- /** Wrap each row in a context menu (right-click). Receives the row meta + trigger element. */
209
- renderContextMenu?: TreeContextMenuSlot<T>;
210
- /**
211
- * Declarative right-click menu — short-form. Pass `(row) => [items]` and the
212
- * Tree builds a `<ContextMenu>` for you with sensible defaults. Ignored if
213
- * `renderContextMenu` is also set. Return `null`/`undefined`/`[]` to skip
214
- * the menu for that row.
215
- */
216
- contextMenuActions?: TreeContextMenuActionsResolver<T>;
217
- /** Override built-in copy in your locale. */
218
- labels?: Partial<TreeLabels>;
219
- /** Persist expanded + (optional) selected ids in localStorage under this key. */
220
- persistKey?: string;
221
- /** Persist selection alongside expansion. Default: false. */
222
- persistSelection?: boolean;
223
- className?: string;
224
- style?: React.CSSProperties;
225
- }
226
- /** Internal flat-row representation used by the renderer + keyboard nav. */
227
- interface FlatRow<T> {
228
- node: TreeNode<T>;
229
- level: number;
230
- parentId: TreeItemId | null;
231
- isFolder: boolean;
232
- isExpanded: boolean;
233
- isLoading: boolean;
234
- hasError: boolean;
235
- /** 1-based position among visible siblings (for `aria-posinset`). */
236
- posInSet: number;
237
- /** Count of visible siblings sharing this row's parent (for `aria-setsize`). */
238
- setSize: number;
239
- }
240
-
241
- export { DEFAULT_TREE_APPEARANCE as D, type FlatRow as F, type ResolvedAppearance as R, type TreeRootProps as T, type TreeItemId as a, type TreeNode as b, type TreeActivateOptions as c, type TreeLabels as d, type TreeSelectionMode as e, type TreeActivationMode as f, type TreeRowSlot as g, type TreeContextMenuSlot as h, appearanceToStyle as i, type TreeAppearance as j, type TreeDensity as k, type TreeAccentIntensity as l, type TreeRadius as m, DEFAULT_TREE_LABELS as n, type TreeRowRenderProps as o, type TreeContextMenuAction as p, type TreeContextMenuItem as q, resolveAppearance as r, type TreeContextMenuActionsResolver as s, type TreeLoadChildren as t };