xertica-ui 2.1.2 → 2.1.4

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 (181) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/bin/cli.ts +1 -1
  4. package/bin/generate-tokens.ts +13 -7
  5. package/components/assistant/xertica-assistant/index.ts +2 -0
  6. package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
  7. package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
  8. package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
  9. package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
  10. package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
  11. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
  12. package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
  13. package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
  14. package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
  15. package/components/assistant/xertica-assistant/parts/index.ts +16 -0
  16. package/components/assistant/xertica-assistant/types.ts +139 -0
  17. package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
  18. package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
  19. package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
  20. package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
  21. package/components/hooks/index.ts +3 -0
  22. package/components/hooks/use-layout-shortcuts.ts +46 -0
  23. package/components/layout/sidebar/index.ts +2 -0
  24. package/components/layout/sidebar/sidebar.stories.tsx +160 -8
  25. package/components/layout/sidebar/sidebar.tsx +606 -497
  26. package/components/layout/sidebar/use-sidebar.ts +104 -0
  27. package/components/media/audio-player/AudioPlayer.tsx +131 -206
  28. package/components/media/audio-player/use-audio-player.ts +298 -0
  29. package/components/pages/home-page/HomePage.tsx +1 -1
  30. package/components/pages/template-content/TemplateContent.tsx +5 -5
  31. package/components/pages/template-page/TemplatePage.tsx +5 -5
  32. package/components/shared/CustomTooltipContent.tsx +52 -0
  33. package/components/shared/layout-constants.ts +1 -1
  34. package/components/ui/chart/chart.stories.tsx +966 -7
  35. package/components/ui/chart/chart.tsx +918 -45
  36. package/components/ui/file-upload/file-upload.stories.tsx +100 -0
  37. package/components/ui/file-upload/file-upload.tsx +14 -74
  38. package/components/ui/file-upload/index.ts +1 -0
  39. package/components/ui/file-upload/use-file-upload.ts +181 -0
  40. package/components/ui/pagination/index.ts +2 -0
  41. package/components/ui/pagination/pagination.stories.tsx +94 -0
  42. package/components/ui/pagination/use-pagination.ts +194 -0
  43. package/components/ui/rich-text-editor/index.ts +2 -0
  44. package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
  45. package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
  46. package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
  47. package/components/ui/stepper/index.ts +3 -1
  48. package/components/ui/stepper/stepper.stories.tsx +116 -0
  49. package/components/ui/stepper/stepper.tsx +4 -4
  50. package/components/ui/stepper/use-stepper.ts +137 -0
  51. package/components/ui/tree-view/index.ts +4 -1
  52. package/components/ui/tree-view/tree-view.stories.tsx +110 -4
  53. package/components/ui/tree-view/tree-view.tsx +17 -125
  54. package/components/ui/tree-view/use-tree-view.ts +229 -0
  55. package/contexts/AssistenteContext.tsx +17 -54
  56. package/contexts/BrandColorsContext.tsx +6 -17
  57. package/contexts/LayoutContext.tsx +5 -31
  58. package/dist/AssistantChart-BAudAfne.cjs +3591 -0
  59. package/dist/AssistantChart-BP8upjMk.js +3565 -0
  60. package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
  61. package/dist/AudioPlayer-DuKXrCfy.js +937 -0
  62. package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
  63. package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
  64. package/dist/LanguageContext-BwhwC3G2.js +657 -0
  65. package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
  66. package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
  67. package/dist/LayoutContext-dbQvdC4O.js +85 -0
  68. package/dist/ThemeContext-RTy1m2Uq.js +82 -0
  69. package/dist/ThemeContext-bSzuOit2.cjs +81 -0
  70. package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
  71. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
  72. package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
  73. package/dist/XerticaProvider-siSt9uG2.js +40 -0
  74. package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
  75. package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
  76. package/dist/assistant.cjs.js +2 -1
  77. package/dist/assistant.es.js +3 -2
  78. package/dist/brand.cjs.js +2 -2
  79. package/dist/brand.es.js +2 -2
  80. package/dist/cli.js +14 -8
  81. package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
  82. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
  83. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
  84. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
  85. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
  86. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
  87. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
  88. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
  89. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
  90. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
  91. package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
  92. package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
  93. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
  94. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
  95. package/dist/components/hooks/index.d.ts +3 -0
  96. package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
  97. package/dist/components/layout/sidebar/index.d.ts +2 -0
  98. package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
  99. package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
  100. package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
  101. package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
  102. package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
  103. package/dist/components/shared/layout-constants.d.ts +1 -1
  104. package/dist/components/ui/alert/alert.d.ts +1 -1
  105. package/dist/components/ui/badge/badge.d.ts +1 -1
  106. package/dist/components/ui/button/button.d.ts +2 -2
  107. package/dist/components/ui/chart/chart.d.ts +162 -5
  108. package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
  109. package/dist/components/ui/file-upload/index.d.ts +1 -0
  110. package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
  111. package/dist/components/ui/pagination/index.d.ts +2 -0
  112. package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
  113. package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
  114. package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
  115. package/dist/components/ui/stepper/index.d.ts +3 -1
  116. package/dist/components/ui/stepper/stepper.d.ts +2 -2
  117. package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
  118. package/dist/components/ui/tree-view/index.d.ts +4 -1
  119. package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
  120. package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
  121. package/dist/contexts/AssistenteContext.d.ts +10 -49
  122. package/dist/hooks.cjs.js +30 -10
  123. package/dist/hooks.es.js +25 -4
  124. package/dist/index.cjs.js +20 -9
  125. package/dist/index.es.js +38 -27
  126. package/dist/layout.cjs.js +82 -1
  127. package/dist/layout.es.js +83 -2
  128. package/dist/media.cjs.js +1 -1
  129. package/dist/media.es.js +1 -1
  130. package/dist/pages.cjs.js +1 -1
  131. package/dist/pages.es.js +1 -1
  132. package/dist/rich-text-editor-BmsjY03B.js +2949 -0
  133. package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
  134. package/dist/sidebar-CVUGHOS_.cjs +756 -0
  135. package/dist/sidebar-CmvwjnVb.js +757 -0
  136. package/dist/ui.cjs.js +12 -2
  137. package/dist/ui.es.js +24 -14
  138. package/dist/use-audio-player-Bkh23vQ3.js +177 -0
  139. package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
  140. package/dist/utils/color-utils.d.ts +51 -0
  141. package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
  142. package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
  143. package/dist/xertica-ui.css +1 -1
  144. package/docs/ai-usage.md +28 -10
  145. package/docs/architecture-improvements.md +463 -0
  146. package/docs/architecture.md +77 -1
  147. package/docs/components/assistant-chart.md +1 -1
  148. package/docs/components/assistant.md +159 -0
  149. package/docs/components/audio-player.md +46 -0
  150. package/docs/components/branding.md +251 -0
  151. package/docs/components/chart.md +354 -39
  152. package/docs/components/code-block.md +108 -0
  153. package/docs/components/file-upload.md +119 -2
  154. package/docs/components/formatted-document.md +113 -0
  155. package/docs/components/hooks.md +430 -0
  156. package/docs/components/image-with-fallback.md +106 -0
  157. package/docs/components/map-layers.md +140 -0
  158. package/docs/components/modern-chat-input.md +163 -0
  159. package/docs/components/pages.md +351 -0
  160. package/docs/components/pagination.md +187 -0
  161. package/docs/components/rich-text-editor.md +164 -0
  162. package/docs/components/sidebar.md +153 -4
  163. package/docs/components/stepper.md +157 -12
  164. package/docs/components/tree-view.md +164 -6
  165. package/docs/doc-audit.md +223 -0
  166. package/docs/getting-started.md +155 -1
  167. package/docs/guidelines.md +14 -8
  168. package/docs/layout.md +2 -2
  169. package/docs/llms.md +29 -9
  170. package/docs/patterns/detail-page.md +276 -0
  171. package/docs/patterns/settings.md +346 -0
  172. package/docs/patterns/wizard.md +217 -0
  173. package/guidelines/Guidelines.md +5 -3
  174. package/llms.txt +1 -1
  175. package/package.json +10 -10
  176. package/styles/xertica/tokens.css +41 -12
  177. package/templates/CLAUDE.md +16 -6
  178. package/templates/guidelines/Guidelines.md +16 -4
  179. package/templates/package.json +3 -3
  180. package/templates/src/styles/xertica/tokens.css +39 -10
  181. package/utils/color-utils.ts +72 -0
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useEffect } from 'react';
1
+ import React from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import {
4
4
  Play,
@@ -11,7 +11,6 @@ import {
11
11
  RotateCcw,
12
12
  Gauge,
13
13
  Info,
14
- RefreshCw,
15
14
  Download,
16
15
  X,
17
16
  ExternalLink,
@@ -33,10 +32,10 @@ import {
33
32
  DropdownMenuItem,
34
33
  DropdownMenuTrigger,
35
34
  DropdownMenuSeparator,
36
- } from "../../ui/dropdown-menu";
35
+ } from '../../ui/dropdown-menu';
37
36
  import { FloatingMediaWrapper } from '../FloatingMediaWrapper';
38
37
  import { cn } from '../../shared/utils';
39
- import { useOptionalLayout } from '../../../contexts/LayoutContext';
38
+ import { useAudioPlayer } from './use-audio-player';
40
39
 
41
40
  export interface AudioPlayerProps {
42
41
  /** Source URL of the audio file */
@@ -73,10 +72,13 @@ export interface AudioPlayerProps {
73
72
 
74
73
  /**
75
74
  * Unified Audio Player component.
75
+ *
76
+ * All state and logic is managed by the `useAudioPlayer` headless hook.
77
+ * This component is responsible only for rendering.
76
78
  */
77
79
  export function AudioPlayer({
78
80
  src,
79
- title = "Áudio",
81
+ title = 'Áudio',
80
82
  artist,
81
83
  subtitle,
82
84
  autoPlay = false,
@@ -88,191 +90,102 @@ export function AudioPlayer({
88
90
  currentTime: initialTime = 0,
89
91
  colorVariant = 'default',
90
92
  enableAutoFloat = true,
91
- onCloseFloating
93
+ onCloseFloating,
92
94
  }: AudioPlayerProps) {
93
- const audioRef = useRef<HTMLAudioElement>(null);
94
- const containerRef = useRef<HTMLDivElement>(null);
95
- const layout = useOptionalLayout();
96
- const sidebarExpanded = layout?.sidebarExpanded ?? false;
97
- const sidebarWidth = layout?.sidebarWidth ?? 256;
98
- const assistenteExpanded = layout?.assistenteExpanded ?? false;
99
-
100
- const [isPlaying, setIsPlaying] = useState(false);
101
- const [volume, setVolume] = useState(0.8);
102
- const [isMuted, setIsMuted] = useState(false);
103
- const [currentTime, setCurrentTime] = useState(initialTime);
104
- const [duration, setDuration] = useState(initialDuration || 0);
105
- const [isFloating, setIsFloating] = useState(false);
106
- const [isManualFloating, setIsManualFloating] = useState(false);
107
- const [enableAutoFloatLocal, setEnableAutoFloatLocal] = useState(enableAutoFloat);
108
- const [isVisible, setIsVisible] = useState(variant === 'card');
109
- const [isMobile, setIsMobile] = useState(false);
110
- const [playbackSpeed, setPlaybackSpeed] = useState(1);
111
-
112
- // Mobile detection
113
- useEffect(() => {
114
- if (typeof window === 'undefined') return;
115
- const checkMobile = () => setIsMobile(window.innerWidth < 768);
116
- checkMobile();
117
- window.addEventListener('resize', checkMobile);
118
- return () => window.removeEventListener('resize', checkMobile);
119
- }, []);
120
-
121
- // Handle visibility for 'bar' variant
122
- useEffect(() => {
123
- if (variant === 'bar') {
124
- if (isOpen) {
125
- setIsVisible(true);
126
- } else {
127
- const timer = setTimeout(() => setIsVisible(false), 300);
128
- return () => clearTimeout(timer);
129
- }
130
- } else {
131
- setIsVisible(true);
132
- }
133
- }, [isOpen, variant]);
134
-
135
- // Sync audio ref with state updates (volume, mute)
136
- useEffect(() => {
137
- const audio = audioRef.current;
138
- if (!audio) return;
139
- audio.volume = volume;
140
- audio.muted = isMuted;
141
- audio.playbackRate = playbackSpeed;
142
- }, [volume, isMuted, playbackSpeed, src]);
143
-
144
- // Auto-float on scroll logic (only for 'card' variant)
145
- useEffect(() => {
146
- if (variant !== 'card' || !enableAutoFloatLocal) return;
147
-
148
- const container = containerRef.current;
149
- if (!container) return;
150
-
151
- if (typeof IntersectionObserver === 'undefined') return;
152
-
153
- const observer = new IntersectionObserver(
154
- ([entry]) => {
155
- if (isPlaying && !entry.isIntersecting && !isFloating) {
156
- if (audioRef.current) setCurrentTime(audioRef.current.currentTime);
157
- setIsFloating(true);
158
- }
159
- else if (entry.isIntersecting && isFloating && !isManualFloating) {
160
- if (audioRef.current) setCurrentTime(audioRef.current.currentTime);
161
- handleSetFloating(false);
162
- }
163
- },
164
- { threshold: 0.2 }
165
- );
166
-
167
- observer.observe(container);
168
- return () => observer.disconnect();
169
- }, [isPlaying, isFloating, isManualFloating, variant, enableAutoFloatLocal]);
170
-
171
- // Restore state when switching modes (remounting audio element)
172
- useEffect(() => {
173
- const audio = audioRef.current;
174
- if (audio) {
175
- if (Math.abs(audio.currentTime - currentTime) > 0.5) {
176
- audio.currentTime = currentTime;
177
- }
178
- if (isPlaying) {
179
- audio.play().catch(err => console.log("Playback interrupted:", err));
180
- }
181
- }
182
- }, [isFloating, variant]);
183
-
184
- const handleSetFloating = (floating: boolean) => {
185
- if (audioRef.current) setCurrentTime(audioRef.current.currentTime);
186
- setIsFloating(floating);
187
- if (!floating) {
188
- setIsManualFloating(false);
189
- }
190
- };
191
-
192
- const togglePlay = () => {
193
- if (audioRef.current) {
194
- if (isPlaying) {
195
- audioRef.current.pause();
196
- } else {
197
- audioRef.current.play().catch(err => console.log("Playback error:", err));
198
- }
199
- }
200
- };
201
-
202
- const toggleMute = () => {
203
- setIsMuted(!isMuted);
204
- };
205
-
206
- const handleSeek = (value: number[]) => {
207
- if (audioRef.current) {
208
- audioRef.current.currentTime = value[0];
209
- setCurrentTime(value[0]);
210
- }
211
- };
212
-
213
- const handleVolumeChange = (value: number[]) => {
214
- const newVolume = value[0];
215
- setVolume(newVolume);
216
- setIsMuted(newVolume === 0);
217
- };
218
-
219
- const formatTime = (time: number) => {
220
- if (isNaN(time) || time === Infinity || !isFinite(time)) return "0:00";
221
- const minutes = Math.floor(time / 60);
222
- const seconds = Math.floor(time % 60);
223
- return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
224
- };
225
-
95
+ const {
96
+ audioRef,
97
+ containerRef,
98
+ isPlaying,
99
+ currentTime,
100
+ duration,
101
+ volume,
102
+ isMuted,
103
+ playbackSpeed,
104
+ isFloating,
105
+ isManualFloating,
106
+ isVisible,
107
+ isMobile,
108
+ enableAutoFloatLocal,
109
+ sidebarExpanded,
110
+ sidebarWidth,
111
+ assistenteExpanded,
112
+ togglePlay,
113
+ toggleMute,
114
+ handleSeek,
115
+ handleVolumeChange,
116
+ handleSetFloating,
117
+ handleEnableManualFloat,
118
+ setPlaybackSpeed,
119
+ resetAudio,
120
+ formatTime,
121
+ onPlay,
122
+ onPause,
123
+ onEnded,
124
+ onTimeUpdate,
125
+ onLoadedMetadata,
126
+ } = useAudioPlayer({
127
+ src,
128
+ autoPlay,
129
+ initialTime,
130
+ initialDuration,
131
+ variant,
132
+ isOpen,
133
+ enableAutoFloat,
134
+ onCloseFloating,
135
+ });
136
+
137
+ // ── Audio element (hidden) ──────────────────────────────────────────────────
226
138
  const audioElement = (
227
139
  <audio
228
140
  ref={audioRef}
229
141
  src={src}
230
142
  autoPlay={autoPlay}
231
143
  className="hidden"
232
- onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
233
- onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}
234
- onPlay={() => setIsPlaying(true)}
235
- onPause={() => setIsPlaying(false)}
236
- onEnded={() => setIsPlaying(false)}
144
+ onTimeUpdate={onTimeUpdate}
145
+ onLoadedMetadata={onLoadedMetadata}
146
+ onPlay={onPlay}
147
+ onPause={onPause}
148
+ onEnded={onEnded}
237
149
  />
238
150
  );
239
151
 
152
+ // ── Bar variant — desktop layout ────────────────────────────────────────────
240
153
  const desktopLayout = (
241
- <div className={cn("px-6 flex items-center justify-between gap-4", colorVariant === 'primary' ? "h-[80px] gap-6" : "h-[80px]")}>
154
+ <div className={cn('px-6 flex items-center justify-between gap-4', colorVariant === 'primary' ? 'h-[80px] gap-6' : 'h-[80px]')}>
242
155
  {/* Left: Info */}
243
- <div className={cn("flex flex-col min-w-0 shrink-0 relative z-10", colorVariant === 'primary' ? "w-[280px]" : "w-[280px]")}>
156
+ <div className={cn('flex flex-col min-w-0 shrink-0 relative z-10', colorVariant === 'primary' ? 'w-[280px]' : 'w-[280px]')}>
244
157
  <div className="flex items-center gap-2 mb-0.5">
245
158
  {colorVariant === 'primary' ? (
246
159
  <div className="w-2 h-2 rounded-full bg-[var(--chart-4)] animate-pulse shrink-0" />
247
160
  ) : (
248
161
  <Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
249
162
  )}
250
- <h4 className={cn("truncate", colorVariant === 'primary' ? "font-semibold text-base tracking-tight" : "font-medium text-base")} title={title}>
163
+ <h4 className={cn('truncate', colorVariant === 'primary' ? 'font-semibold text-base tracking-tight' : 'font-medium text-base')} title={title}>
251
164
  {title}
252
165
  </h4>
253
166
  </div>
254
- <div className={cn("flex items-center gap-1 text-xs text-muted-foreground", colorVariant === 'primary' && "opacity-80")}>
167
+ <div className={cn('flex items-center gap-1 text-xs text-muted-foreground', colorVariant === 'primary' && 'opacity-80')}>
255
168
  <span className="truncate">{subtitle || artist}</span>
256
- <ExternalLink className={cn("w-3 h-3 cursor-pointer hover:text-foreground ml-1 transition-opacity", colorVariant === 'primary' ? "opacity-60 hover:opacity-100" : "opacity-70 hover:opacity-100")} />
169
+ <ExternalLink className={cn('w-3 h-3 cursor-pointer hover:text-foreground ml-1 transition-opacity', colorVariant === 'primary' ? 'opacity-60 hover:opacity-100' : 'opacity-70 hover:opacity-100')} />
257
170
  </div>
258
171
  </div>
259
172
 
260
173
  {/* Center: Controls */}
261
- <div className={cn("flex-1 flex items-center justify-center gap-4 min-w-0 max-w-xl relative z-10", colorVariant === 'primary' && "gap-4")}>
174
+ <div className={cn('flex-1 flex items-center justify-center gap-4 min-w-0 max-w-xl relative z-10', colorVariant === 'primary' && 'gap-4')}>
262
175
  <button
263
176
  onClick={togglePlay}
264
- aria-label={isPlaying ? "Pausar" : "Reproduzir"}
265
- className={cn("transition-all focus:outline-none shrink-0", colorVariant === 'primary' ? "hover:scale-110" : "text-muted-foreground hover:text-foreground hover:scale-110 active:scale-95 transform duration-100")}
177
+ aria-label={isPlaying ? 'Pausar' : 'Reproduzir'}
178
+ className={cn('transition-all focus:outline-none shrink-0', colorVariant === 'primary' ? 'hover:scale-110' : 'text-muted-foreground hover:text-foreground hover:scale-110 active:scale-95 transform duration-100')}
266
179
  >
267
180
  {isPlaying ? (
268
- <PauseCircle className={cn(colorVariant === 'primary' ? "w-12 h-12" : "w-10 h-10")} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
181
+ <PauseCircle className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
269
182
  ) : (
270
- <PlayCircle className={cn(colorVariant === 'primary' ? "w-12 h-12" : "w-10 h-10")} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
183
+ <PlayCircle className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
271
184
  )}
272
185
  </button>
273
186
 
274
187
  <div className="flex-1 flex items-center gap-3 max-w-lg">
275
- <span className={cn("text-xs font-mono shrink-0 w-10 text-right", colorVariant === 'primary' ? "text-[11px] w-12 opacity-80" : "text-muted-foreground")}>
188
+ <span className={cn('text-xs font-mono shrink-0 w-10 text-right', colorVariant === 'primary' ? 'text-[11px] w-12 opacity-80' : 'text-muted-foreground')}>
276
189
  {formatTime(currentTime)}
277
190
  </span>
278
191
  <Slider
@@ -281,7 +194,7 @@ export function AudioPlayer({
281
194
  onValueChange={handleSeek}
282
195
  className="cursor-pointer"
283
196
  />
284
- <span className={cn("text-xs font-mono shrink-0 w-10", colorVariant === 'primary' ? "text-[11px] w-12 opacity-80" : "text-muted-foreground")}>
197
+ <span className={cn('text-xs font-mono shrink-0 w-10', colorVariant === 'primary' ? 'text-[11px] w-12 opacity-80' : 'text-muted-foreground')}>
285
198
  {formatTime(duration)}
286
199
  </span>
287
200
  </div>
@@ -293,7 +206,7 @@ export function AudioPlayer({
293
206
  <button
294
207
  onClick={toggleMute}
295
208
  className="text-muted-foreground hover:text-foreground"
296
- aria-label={isMuted || volume === 0 ? "Ativar som" : "Desativar som"}
209
+ aria-label={isMuted || volume === 0 ? 'Ativar som' : 'Desativar som'}
297
210
  >
298
211
  {isMuted || volume === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
299
212
  </button>
@@ -310,7 +223,7 @@ export function AudioPlayer({
310
223
  <TooltipProvider>
311
224
  <Tooltip>
312
225
  <TooltipTrigger asChild>
313
- <Button variant="ghost" size="icon" className="rounded-full h-9 w-9" onClick={() => { if (audioRef.current) audioRef.current.currentTime = 0; }} aria-label="Recomecar audio">
226
+ <Button variant="ghost" size="icon" className="rounded-full h-9 w-9" onClick={resetAudio} aria-label="Recomecar audio">
314
227
  <RotateCcw className="w-4 h-4" />
315
228
  </Button>
316
229
  </TooltipTrigger>
@@ -342,7 +255,7 @@ export function AudioPlayer({
342
255
  <DropdownMenuItem
343
256
  key={speed}
344
257
  onClick={() => setPlaybackSpeed(speed)}
345
- className={cn("cursor-pointer", playbackSpeed === speed && "bg-accent font-bold")}
258
+ className={cn('cursor-pointer', playbackSpeed === speed && 'bg-accent font-bold')}
346
259
  >
347
260
  {speed}x
348
261
  </DropdownMenuItem>
@@ -384,7 +297,7 @@ export function AudioPlayer({
384
297
  </div>
385
298
 
386
299
  <div className="md:hidden">
387
- <DropdownMenuItem onClick={() => { if (audioRef.current) audioRef.current.currentTime = 0; }} className="cursor-pointer py-2.5">
300
+ <DropdownMenuItem onClick={resetAudio} className="cursor-pointer py-2.5">
388
301
  <RotateCcw className="w-4 h-4 mr-3 text-muted-foreground" /> Recomeçar
389
302
  </DropdownMenuItem>
390
303
  <DropdownMenuItem className="cursor-pointer py-2.5">
@@ -405,8 +318,8 @@ export function AudioPlayer({
405
318
  key={speed}
406
319
  onClick={() => setPlaybackSpeed(speed)}
407
320
  className={cn(
408
- "px-2 py-0.5 text-[10px] rounded border transition-colors",
409
- playbackSpeed === speed ? "bg-primary text-primary-foreground border-primary" : "bg-transparent border-border hover:bg-accent"
321
+ 'px-2 py-0.5 text-[10px] rounded border transition-colors',
322
+ playbackSpeed === speed ? 'bg-primary text-primary-foreground border-primary' : 'bg-transparent border-border hover:bg-accent'
410
323
  )}
411
324
  >
412
325
  {speed}x
@@ -433,8 +346,9 @@ export function AudioPlayer({
433
346
  </div>
434
347
  );
435
348
 
349
+ // ── Bar variant — mobile layout ─────────────────────────────────────────────
436
350
  const mobileLayout = (
437
- <div className={cn("flex flex-col py-3 px-4 gap-3", colorVariant === 'primary' && "py-4 px-5 gap-4")}>
351
+ <div className={cn('flex flex-col py-3 px-4 gap-3', colorVariant === 'primary' && 'py-4 px-5 gap-4')}>
438
352
  <div className="flex items-start justify-between gap-3">
439
353
  <div className="flex flex-col min-w-0 flex-1">
440
354
  <div className="flex items-center gap-2 mb-1">
@@ -443,9 +357,9 @@ export function AudioPlayer({
443
357
  ) : (
444
358
  <Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse shrink-0" />
445
359
  )}
446
- <h4 className={cn("truncate", colorVariant === 'primary' ? "font-semibold text-sm tracking-tight" : "font-medium text-sm")}>{title}</h4>
360
+ <h4 className={cn('truncate', colorVariant === 'primary' ? 'font-semibold text-sm tracking-tight' : 'font-medium text-sm')}>{title}</h4>
447
361
  </div>
448
- <div className={cn("flex items-center gap-1.5 text-xs text-muted-foreground", colorVariant === 'primary' && "opacity-70")}>
362
+ <div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', colorVariant === 'primary' && 'opacity-70')}>
449
363
  <span className="truncate">{subtitle || artist}</span>
450
364
  </div>
451
365
  </div>
@@ -455,20 +369,20 @@ export function AudioPlayer({
455
369
  </div>
456
370
 
457
371
  <div className="flex items-center gap-3">
458
- <button onClick={togglePlay} aria-label={isPlaying ? "Pausar" : "Reproduzir"} className="shrink-0 transition-transform active:scale-95">
372
+ <button onClick={togglePlay} aria-label={isPlaying ? 'Pausar' : 'Reproduzir'} className="shrink-0 transition-transform active:scale-95">
459
373
  {isPlaying ? (
460
- <PauseCircle className={cn(colorVariant === 'primary' ? "w-12 h-12" : "w-10 h-10")} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
374
+ <PauseCircle className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
461
375
  ) : (
462
- <PlayCircle className={cn(colorVariant === 'primary' ? "w-12 h-12" : "w-10 h-10")} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
376
+ <PlayCircle className={cn(colorVariant === 'primary' ? 'w-12 h-12' : 'w-10 h-10')} strokeWidth={colorVariant === 'primary' ? 1 : 1.5} />
463
377
  )}
464
378
  </button>
465
379
 
466
380
  <div className="flex-1 flex items-center gap-2">
467
- <Slider
468
- value={[currentTime]}
469
- max={duration || 100}
470
- onValueChange={handleSeek}
471
- className="cursor-pointer"
381
+ <Slider
382
+ value={[currentTime]}
383
+ max={duration || 100}
384
+ onValueChange={handleSeek}
385
+ className="cursor-pointer"
472
386
  aria-label="Progresso da reprodução"
473
387
  />
474
388
  </div>
@@ -480,33 +394,34 @@ export function AudioPlayer({
480
394
  </Button>
481
395
  </DropdownMenuTrigger>
482
396
  <DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
483
- <DropdownMenuItem onClick={() => { if (audioRef.current) audioRef.current.currentTime = 0; }}>Recomeçar</DropdownMenuItem>
484
- <DropdownMenuItem>Velocidade (1.0x)</DropdownMenuItem>
397
+ <DropdownMenuItem onClick={resetAudio}>Recomeçar</DropdownMenuItem>
398
+ <DropdownMenuItem>Velocidade ({playbackSpeed}x)</DropdownMenuItem>
485
399
  </DropdownMenuContent>
486
400
  </DropdownMenu>
487
401
  </div>
488
402
  </div>
489
403
  );
490
404
 
405
+ // ── Card variant — compact layout ───────────────────────────────────────────
491
406
  const desktopLayoutCompact = (
492
- <div className={cn("flex items-center gap-3 w-full", isFloating ? "px-3 py-1 text-foreground" : "px-4 py-3")}>
407
+ <div className={cn('flex items-center gap-3 w-full', isFloating ? 'px-3 py-1 text-foreground' : 'px-4 py-3')}>
493
408
  <Button
494
409
  onClick={togglePlay}
495
410
  size="icon"
496
411
  variant="outline"
497
412
  className={cn(
498
- "shrink-0 rounded-full border-border text-foreground hover:bg-accent transition-colors",
499
- colorVariant === 'primary' && "border-primary-foreground/20 hover:bg-primary-foreground/10 text-primary-foreground",
500
- isFloating ? "h-8 w-8" : "h-10 w-10"
413
+ 'shrink-0 rounded-full border-border text-foreground hover:bg-accent transition-colors',
414
+ colorVariant === 'primary' && 'border-primary-foreground/20 hover:bg-primary-foreground/10 text-primary-foreground',
415
+ isFloating ? 'h-8 w-8' : 'h-10 w-10'
501
416
  )}
502
- aria-label={isPlaying ? "Pausar" : "Reproduzir"}
417
+ aria-label={isPlaying ? 'Pausar' : 'Reproduzir'}
503
418
  >
504
- {isPlaying ? <Pause className={cn(isFloating ? "w-3 h-3" : "w-4 h-4", "fill-current")} /> : <Play className={cn(isFloating ? "w-3 h-3" : "w-4 h-4", "fill-current ml-0.5")} />}
419
+ {isPlaying ? <Pause className={cn(isFloating ? 'w-3 h-3' : 'w-4 h-4', 'fill-current')} /> : <Play className={cn(isFloating ? 'w-3 h-3' : 'w-4 h-4', 'fill-current ml-0.5')} />}
505
420
  </Button>
506
421
  <div className="flex-1 min-w-0 flex flex-col justify-center gap-1.5">
507
422
  <div className="flex items-center justify-between text-xs leading-none">
508
- <span className={cn("font-medium truncate", colorVariant === 'primary' ? "text-primary-foreground" : "text-foreground")}>{title}</span>
509
- <span className={cn("font-medium tabular-nums text-[10px] sm:text-xs", colorVariant === 'primary' ? "text-primary-foreground/70" : "text-muted-foreground")}>
423
+ <span className={cn('font-medium truncate', colorVariant === 'primary' ? 'text-primary-foreground' : 'text-foreground')}>{title}</span>
424
+ <span className={cn('font-medium tabular-nums text-[10px] sm:text-xs', colorVariant === 'primary' ? 'text-primary-foreground/70' : 'text-muted-foreground')}>
510
425
  {formatTime(currentTime)} / {formatTime(duration)}
511
426
  </span>
512
427
  </div>
@@ -523,8 +438,17 @@ export function AudioPlayer({
523
438
  <div className="flex items-center gap-1 shrink-0">
524
439
  <DropdownMenu>
525
440
  <DropdownMenuTrigger asChild>
526
- <Button variant="ghost" size="icon" className={cn("rounded-full", colorVariant === 'primary' ? "text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10" : "text-muted-foreground hover:text-foreground", isFloating ? "h-7 w-7" : "h-8 w-8")} aria-label="Mais opções">
527
- <MoreVertical className={isFloating ? "w-4 h-4" : "w-5 h-5"} />
441
+ <Button
442
+ variant="ghost"
443
+ size="icon"
444
+ className={cn(
445
+ 'rounded-full',
446
+ colorVariant === 'primary' ? 'text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10' : 'text-muted-foreground hover:text-foreground',
447
+ isFloating ? 'h-7 w-7' : 'h-8 w-8'
448
+ )}
449
+ aria-label="Mais opções"
450
+ >
451
+ <MoreVertical className={isFloating ? 'w-4 h-4' : 'w-5 h-5'} />
528
452
  </Button>
529
453
  </DropdownMenuTrigger>
530
454
  <DropdownMenuContent align="end" className="w-56 p-2 z-[1001]">
@@ -556,8 +480,8 @@ export function AudioPlayer({
556
480
  key={speed}
557
481
  onClick={() => setPlaybackSpeed(speed)}
558
482
  className={cn(
559
- "px-2 py-1 text-[10px] rounded border transition-colors",
560
- playbackSpeed === speed ? "bg-primary text-primary-foreground border-primary" : "bg-transparent border-border hover:bg-accent"
483
+ 'px-2 py-1 text-[10px] rounded border transition-colors',
484
+ playbackSpeed === speed ? 'bg-primary text-primary-foreground border-primary' : 'bg-transparent border-border hover:bg-accent'
561
485
  )}
562
486
  >
563
487
  {speed}x
@@ -568,7 +492,7 @@ export function AudioPlayer({
568
492
 
569
493
  <DropdownMenuSeparator className="my-2" />
570
494
 
571
- <DropdownMenuItem onClick={() => { if (audioRef.current) audioRef.current.currentTime = 0; }} className="cursor-pointer py-2.5">
495
+ <DropdownMenuItem onClick={resetAudio} className="cursor-pointer py-2.5">
572
496
  <RotateCcw className="w-4 h-4 mr-3 text-muted-foreground" />
573
497
  <span className="text-sm">Recomeçar áudio</span>
574
498
  </DropdownMenuItem>
@@ -584,12 +508,11 @@ export function AudioPlayer({
584
508
  <Button
585
509
  variant="ghost"
586
510
  size="icon"
587
- className={cn("h-8 w-8", colorVariant === 'primary' ? "text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10" : "text-muted-foreground hover:text-foreground")}
588
- onClick={() => {
589
- setEnableAutoFloatLocal(true);
590
- setIsManualFloating(true);
591
- handleSetFloating(true);
592
- }}
511
+ className={cn(
512
+ 'h-8 w-8',
513
+ colorVariant === 'primary' ? 'text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/10' : 'text-muted-foreground hover:text-foreground'
514
+ )}
515
+ onClick={handleEnableManualFloat}
593
516
  title="Modo Flutuante"
594
517
  aria-label="Abrir modo flutuante"
595
518
  >
@@ -600,22 +523,24 @@ export function AudioPlayer({
600
523
  </div>
601
524
  );
602
525
 
526
+ // ── Early return ────────────────────────────────────────────────────────────
603
527
  if (!isVisible) return null;
604
528
 
529
+ // ── Bar variant — portal render ─────────────────────────────────────────────
605
530
  if (variant === 'bar' && isVisible && typeof document !== 'undefined') {
606
531
  return createPortal(
607
532
  <div
608
533
  className={cn(
609
- "fixed bottom-0 z-[100] border-t transition-all duration-300 ease-in-out",
534
+ 'fixed bottom-0 z-[100] border-t transition-all duration-300 ease-in-out',
610
535
  colorVariant === 'primary'
611
- ? "bg-primary/95 text-primary-foreground border-primary/20 backdrop-blur-sm [&_.text-muted-foreground]:text-primary-foreground/70 [&_.text-foreground]:text-primary-foreground [&_button]:text-primary-foreground [&_[data-slot=slider-track]]:bg-primary-foreground/30 [&_[data-slot=slider-range]]:bg-primary-foreground [&_[data-slot=slider-thumb]]:border-primary-foreground [&_[data-slot=slider-thumb]]:bg-primary-foreground"
612
- : "bg-card/95 text-card-foreground border-border backdrop-blur-md shadow-[var(--elevation-sm)]",
613
- isOpen ? "translate-y-0 opacity-100" : "translate-y-full opacity-0",
536
+ ? 'bg-primary/95 text-primary-foreground border-primary/20 backdrop-blur-sm [&_.text-muted-foreground]:text-primary-foreground/70 [&_.text-foreground]:text-primary-foreground [&_button]:text-primary-foreground [&_[data-slot=slider-track]]:bg-primary-foreground/30 [&_[data-slot=slider-range]]:bg-primary-foreground [&_[data-slot=slider-thumb]]:border-primary-foreground [&_[data-slot=slider-thumb]]:bg-primary-foreground'
537
+ : 'bg-card/95 text-card-foreground border-border backdrop-blur-md shadow-[var(--elevation-sm)]',
538
+ isOpen ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0',
614
539
  className
615
540
  )}
616
541
  style={{
617
542
  left: isMobile ? 0 : (sidebarExpanded ? sidebarWidth : 80),
618
- right: isMobile ? 0 : (assistenteExpanded ? 420 : 80)
543
+ right: isMobile ? 0 : (assistenteExpanded ? 420 : 80),
619
544
  }}
620
545
  >
621
546
  {audioElement}
@@ -625,6 +550,7 @@ export function AudioPlayer({
625
550
  );
626
551
  }
627
552
 
553
+ // ── Card variant — floating wrapper render ──────────────────────────────────
628
554
  return (
629
555
  <div ref={containerRef} className={className}>
630
556
  {audioElement}
@@ -633,8 +559,7 @@ export function AudioPlayer({
633
559
  setIsFloating={handleSetFloating}
634
560
  onClose={() => handleSetFloating(false)}
635
561
  onCloseMedia={() => {
636
- setIsFloating(false);
637
- setEnableAutoFloatLocal(false);
562
+ handleSetFloating(false);
638
563
  }}
639
564
  title="Reproduzindo Mídia"
640
565
  aspectRatio={320 / 110}
@@ -646,13 +571,13 @@ export function AudioPlayer({
646
571
  enablePadding
647
572
  >
648
573
  <div className={cn(
649
- "w-full overflow-hidden flex flex-col justify-center",
574
+ 'w-full overflow-hidden flex flex-col justify-center',
650
575
  colorVariant === 'primary'
651
- ? "bg-primary text-primary-foreground [&_[data-slot=slider-track]]:bg-white/20 [&_[data-slot=slider-range]]:bg-white [&_[data-slot=slider-thumb]]:border-white [&_[data-slot=slider-thumb]]:bg-white"
652
- : "bg-card text-card-foreground",
576
+ ? 'bg-primary text-primary-foreground [&_[data-slot=slider-track]]:bg-white/20 [&_[data-slot=slider-range]]:bg-white [&_[data-slot=slider-thumb]]:border-white [&_[data-slot=slider-thumb]]:bg-white'
577
+ : 'bg-card text-card-foreground',
653
578
  isFloating
654
- ? "h-full bg-transparent"
655
- : "border rounded-md shadow-sm"
579
+ ? 'h-full bg-transparent'
580
+ : 'border rounded-md shadow-sm'
656
581
  )}>
657
582
  {desktopLayoutCompact}
658
583
  </div>