ui-svelte 0.1.0

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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/charts/ArcChart.svelte +320 -0
  4. package/dist/charts/ArcChart.svelte.d.ts +26 -0
  5. package/dist/charts/AreaChart.svelte +495 -0
  6. package/dist/charts/AreaChart.svelte.d.ts +32 -0
  7. package/dist/charts/BarChart.svelte +504 -0
  8. package/dist/charts/BarChart.svelte.d.ts +38 -0
  9. package/dist/charts/Candlestick.svelte +527 -0
  10. package/dist/charts/Candlestick.svelte.d.ts +38 -0
  11. package/dist/charts/LineChart.svelte +365 -0
  12. package/dist/charts/LineChart.svelte.d.ts +36 -0
  13. package/dist/charts/PieChart.svelte +311 -0
  14. package/dist/charts/PieChart.svelte.d.ts +28 -0
  15. package/dist/charts/css/arc-chart.css +237 -0
  16. package/dist/charts/css/area-chart.css +289 -0
  17. package/dist/charts/css/bar-chart.css +167 -0
  18. package/dist/charts/css/candlestick.css +197 -0
  19. package/dist/charts/css/line-chart.css +202 -0
  20. package/dist/charts/css/pie-chart.css +199 -0
  21. package/dist/control/Audio.svelte +212 -0
  22. package/dist/control/Audio.svelte.d.ts +8 -0
  23. package/dist/control/Button.svelte +116 -0
  24. package/dist/control/Button.svelte.d.ts +22 -0
  25. package/dist/control/IconButton.svelte +104 -0
  26. package/dist/control/IconButton.svelte.d.ts +17 -0
  27. package/dist/control/Record.svelte +430 -0
  28. package/dist/control/Record.svelte.d.ts +11 -0
  29. package/dist/control/ToggleTheme.svelte +21 -0
  30. package/dist/control/ToggleTheme.svelte.d.ts +8 -0
  31. package/dist/control/Video.svelte +222 -0
  32. package/dist/control/Video.svelte.d.ts +10 -0
  33. package/dist/control/css/btn.css +206 -0
  34. package/dist/control/css/media.css +78 -0
  35. package/dist/control/css/video.css +58 -0
  36. package/dist/css/animations.css +27 -0
  37. package/dist/css/base.css +192 -0
  38. package/dist/css/utilities.css +136 -0
  39. package/dist/display/Accordion.svelte +98 -0
  40. package/dist/display/Accordion.svelte.d.ts +20 -0
  41. package/dist/display/Alert.svelte +65 -0
  42. package/dist/display/Alert.svelte.d.ts +15 -0
  43. package/dist/display/Avatar.svelte +80 -0
  44. package/dist/display/Avatar.svelte.d.ts +13 -0
  45. package/dist/display/Badge.svelte +46 -0
  46. package/dist/display/Badge.svelte.d.ts +11 -0
  47. package/dist/display/Card.svelte +94 -0
  48. package/dist/display/Card.svelte.d.ts +21 -0
  49. package/dist/display/Carousel.svelte +359 -0
  50. package/dist/display/Carousel.svelte.d.ts +25 -0
  51. package/dist/display/ChatBox.svelte +249 -0
  52. package/dist/display/ChatBox.svelte.d.ts +18 -0
  53. package/dist/display/Chip.svelte +67 -0
  54. package/dist/display/Chip.svelte.d.ts +17 -0
  55. package/dist/display/Code.svelte +56 -0
  56. package/dist/display/Code.svelte.d.ts +9 -0
  57. package/dist/display/Collapsible.svelte +71 -0
  58. package/dist/display/Collapsible.svelte.d.ts +15 -0
  59. package/dist/display/Divider.svelte +32 -0
  60. package/dist/display/Divider.svelte.d.ts +10 -0
  61. package/dist/display/Empty.svelte +462 -0
  62. package/dist/display/Empty.svelte.d.ts +11 -0
  63. package/dist/display/Icon.svelte +20 -0
  64. package/dist/display/Icon.svelte.d.ts +11 -0
  65. package/dist/display/Item.svelte +119 -0
  66. package/dist/display/Item.svelte.d.ts +24 -0
  67. package/dist/display/Loading.svelte +8 -0
  68. package/dist/display/Loading.svelte.d.ts +26 -0
  69. package/dist/display/Marquee.svelte +164 -0
  70. package/dist/display/Marquee.svelte.d.ts +21 -0
  71. package/dist/display/Section.svelte +63 -0
  72. package/dist/display/Section.svelte.d.ts +16 -0
  73. package/dist/display/Table.svelte +407 -0
  74. package/dist/display/Table.svelte.d.ts +32 -0
  75. package/dist/display/TypeWriter.svelte +23 -0
  76. package/dist/display/TypeWriter.svelte.d.ts +11 -0
  77. package/dist/display/User.svelte +0 -0
  78. package/dist/display/User.svelte.d.ts +26 -0
  79. package/dist/display/css/accordion.css +98 -0
  80. package/dist/display/css/alert.css +51 -0
  81. package/dist/display/css/avatar.css +158 -0
  82. package/dist/display/css/badge.css +47 -0
  83. package/dist/display/css/card.css +231 -0
  84. package/dist/display/css/carousel.css +156 -0
  85. package/dist/display/css/chat-box.css +188 -0
  86. package/dist/display/css/chip.css +91 -0
  87. package/dist/display/css/code.css +19 -0
  88. package/dist/display/css/collapsible.css +86 -0
  89. package/dist/display/css/divider.css +54 -0
  90. package/dist/display/css/empty.css +8 -0
  91. package/dist/display/css/item.css +149 -0
  92. package/dist/display/css/listbox.css +24 -0
  93. package/dist/display/css/marquee.css +138 -0
  94. package/dist/display/css/section.css +85 -0
  95. package/dist/display/css/table.css +361 -0
  96. package/dist/form/Checkbox.svelte +45 -0
  97. package/dist/form/Checkbox.svelte.d.ts +13 -0
  98. package/dist/form/ComboBox.svelte +448 -0
  99. package/dist/form/ComboBox.svelte.d.ts +29 -0
  100. package/dist/form/CsvField.svelte +389 -0
  101. package/dist/form/CsvField.svelte.d.ts +21 -0
  102. package/dist/form/DateField.svelte +292 -0
  103. package/dist/form/DateField.svelte.d.ts +18 -0
  104. package/dist/form/Dropzone.svelte +196 -0
  105. package/dist/form/Dropzone.svelte.d.ts +30 -0
  106. package/dist/form/ImageCropper.svelte +254 -0
  107. package/dist/form/ImageCropper.svelte.d.ts +14 -0
  108. package/dist/form/PasswordField.svelte +170 -0
  109. package/dist/form/PasswordField.svelte.d.ts +28 -0
  110. package/dist/form/PhoneField.svelte +485 -0
  111. package/dist/form/PhoneField.svelte.d.ts +25 -0
  112. package/dist/form/PinField.svelte +139 -0
  113. package/dist/form/PinField.svelte.d.ts +17 -0
  114. package/dist/form/RadioGroup.svelte +70 -0
  115. package/dist/form/RadioGroup.svelte.d.ts +19 -0
  116. package/dist/form/Select.svelte +350 -0
  117. package/dist/form/Select.svelte.d.ts +26 -0
  118. package/dist/form/Slider.svelte +60 -0
  119. package/dist/form/Slider.svelte.d.ts +15 -0
  120. package/dist/form/TextField.svelte +154 -0
  121. package/dist/form/TextField.svelte.d.ts +31 -0
  122. package/dist/form/Textarea.svelte +137 -0
  123. package/dist/form/Textarea.svelte.d.ts +27 -0
  124. package/dist/form/Toggle.svelte +45 -0
  125. package/dist/form/Toggle.svelte.d.ts +13 -0
  126. package/dist/form/css/checkbox.css +46 -0
  127. package/dist/form/css/combo-box.css +69 -0
  128. package/dist/form/css/control.css +177 -0
  129. package/dist/form/css/csv-field.css +0 -0
  130. package/dist/form/css/date.css +56 -0
  131. package/dist/form/css/dropzone.css +133 -0
  132. package/dist/form/css/field.css +17 -0
  133. package/dist/form/css/image-cropper.css +155 -0
  134. package/dist/form/css/password.css +35 -0
  135. package/dist/form/css/radio-group.css +57 -0
  136. package/dist/form/css/select.css +18 -0
  137. package/dist/form/css/slider.css +80 -0
  138. package/dist/form/css/textarea.css +130 -0
  139. package/dist/form/css/toggle.css +27 -0
  140. package/dist/form/js/countries.d.ts +13 -0
  141. package/dist/form/js/countries.js +307 -0
  142. package/dist/form/js/phone-examples.d.ts +248 -0
  143. package/dist/form/js/phone-examples.js +247 -0
  144. package/dist/hooks/use-auth.svelte.d.ts +11 -0
  145. package/dist/hooks/use-auth.svelte.js +59 -0
  146. package/dist/hooks/use-chat.svelte.d.ts +40 -0
  147. package/dist/hooks/use-chat.svelte.js +265 -0
  148. package/dist/hooks/use-clipboard.svelte.d.ts +9 -0
  149. package/dist/hooks/use-clipboard.svelte.js +52 -0
  150. package/dist/hooks/use-fetch.svelte.d.ts +11 -0
  151. package/dist/hooks/use-fetch.svelte.js +38 -0
  152. package/dist/hooks/use-form.svelte.d.ts +31 -0
  153. package/dist/hooks/use-form.svelte.js +110 -0
  154. package/dist/hooks/use-localstorage.svelte.d.ts +3 -0
  155. package/dist/hooks/use-localstorage.svelte.js +26 -0
  156. package/dist/hooks/use-scroll.svelte.d.ts +6 -0
  157. package/dist/hooks/use-scroll.svelte.js +34 -0
  158. package/dist/hooks/use-search.svelte.d.ts +49 -0
  159. package/dist/hooks/use-search.svelte.js +229 -0
  160. package/dist/hooks/use-table.svelte.d.ts +85 -0
  161. package/dist/hooks/use-table.svelte.js +362 -0
  162. package/dist/hooks/use-websocket.svelte.d.ts +18 -0
  163. package/dist/hooks/use-websocket.svelte.js +79 -0
  164. package/dist/icons/index.d.ts +132 -0
  165. package/dist/icons/index.js +132 -0
  166. package/dist/index.css +115 -0
  167. package/dist/index.d.ts +76 -0
  168. package/dist/index.js +76 -0
  169. package/dist/layout/AppBar.svelte +94 -0
  170. package/dist/layout/AppBar.svelte.d.ts +17 -0
  171. package/dist/layout/Footer.svelte +94 -0
  172. package/dist/layout/Footer.svelte.d.ts +17 -0
  173. package/dist/layout/FooterLinks.svelte +28 -0
  174. package/dist/layout/FooterLinks.svelte.d.ts +11 -0
  175. package/dist/layout/Provider.svelte +52 -0
  176. package/dist/layout/Provider.svelte.d.ts +10 -0
  177. package/dist/layout/Scaffold.svelte +46 -0
  178. package/dist/layout/Scaffold.svelte.d.ts +15 -0
  179. package/dist/layout/Sidebar.svelte +40 -0
  180. package/dist/layout/Sidebar.svelte.d.ts +13 -0
  181. package/dist/layout/css/app-bar.css +35 -0
  182. package/dist/layout/css/bottom-bar.css +12 -0
  183. package/dist/layout/css/footer-links.css +17 -0
  184. package/dist/layout/css/footer.css +35 -0
  185. package/dist/layout/css/scaffold.css +15 -0
  186. package/dist/layout/css/sidebar.css +17 -0
  187. package/dist/navigation/BottomNav.svelte +0 -0
  188. package/dist/navigation/BottomNav.svelte.d.ts +26 -0
  189. package/dist/navigation/NavMenu.svelte +254 -0
  190. package/dist/navigation/SideNav.svelte +249 -0
  191. package/dist/navigation/Tabs.svelte +79 -0
  192. package/dist/navigation/Tabs.svelte.d.ts +19 -0
  193. package/dist/navigation/css/bottom-nav.css +0 -0
  194. package/dist/navigation/css/nav-menu.css +168 -0
  195. package/dist/navigation/css/side-nav.css +244 -0
  196. package/dist/navigation/css/tabs.css +118 -0
  197. package/dist/overlay/AlertDialog.svelte +0 -0
  198. package/dist/overlay/AlertDialog.svelte.d.ts +26 -0
  199. package/dist/overlay/Command.svelte +0 -0
  200. package/dist/overlay/Command.svelte.d.ts +26 -0
  201. package/dist/overlay/Drawer.svelte +129 -0
  202. package/dist/overlay/Drawer.svelte.d.ts +20 -0
  203. package/dist/overlay/Dropdown.svelte +140 -0
  204. package/dist/overlay/Modal.svelte +102 -0
  205. package/dist/overlay/Modal.svelte.d.ts +19 -0
  206. package/dist/overlay/PopoverStack.svelte +0 -0
  207. package/dist/overlay/PopoverStack.svelte.d.ts +26 -0
  208. package/dist/overlay/Toast.svelte +83 -0
  209. package/dist/overlay/Toast.svelte.d.ts +9 -0
  210. package/dist/overlay/Tooltip.svelte +140 -0
  211. package/dist/overlay/Tooltip.svelte.d.ts +12 -0
  212. package/dist/overlay/css/drawer.css +75 -0
  213. package/dist/overlay/css/dropdown.css +24 -0
  214. package/dist/overlay/css/hovercard.css +11 -0
  215. package/dist/overlay/css/modal.css +51 -0
  216. package/dist/overlay/css/toast.css +80 -0
  217. package/dist/overlay/css/tooltip.css +89 -0
  218. package/dist/stores/i18n.svelte.d.ts +16 -0
  219. package/dist/stores/i18n.svelte.js +137 -0
  220. package/dist/stores/theme.svelte.d.ts +5 -0
  221. package/dist/stores/theme.svelte.js +55 -0
  222. package/dist/stores/toast.svelte.d.ts +19 -0
  223. package/dist/stores/toast.svelte.js +38 -0
  224. package/dist/types.d.ts +75 -0
  225. package/dist/types.js +1 -0
  226. package/dist/utils/charts.d.ts +27 -0
  227. package/dist/utils/charts.js +140 -0
  228. package/dist/utils/class-names.d.ts +1 -0
  229. package/dist/utils/class-names.js +3 -0
  230. package/dist/utils/click-outside.d.ts +3 -0
  231. package/dist/utils/click-outside.js +9 -0
  232. package/dist/utils/popover.d.ts +3 -0
  233. package/dist/utils/popover.js +17 -0
  234. package/dist/utils/ulid.d.ts +1 -0
  235. package/dist/utils/ulid.js +22 -0
  236. package/dist/utils/validate-schema.d.ts +2 -0
  237. package/dist/utils/validate-schema.js +97 -0
  238. package/package.json +69 -0
@@ -0,0 +1,430 @@
1
+ <script lang="ts">
2
+ import {
3
+ Checkmark24RegularIcon,
4
+ Delete24RegularIcon,
5
+ Pause24RegularIcon,
6
+ Play24RegularIcon,
7
+ Record24RegularIcon,
8
+ RecordStop24RegularIcon
9
+ } from '../icons/index.js';
10
+ import { Button, Icon } from '../index.js';
11
+ import { cn } from '../utils/class-names.js';
12
+
13
+ type Props = {
14
+ class?: string;
15
+ name: string;
16
+ variant?:
17
+ | 'primary'
18
+ | 'secondary'
19
+ | 'muted'
20
+ | 'outlined'
21
+ | 'ghost'
22
+ | 'success'
23
+ | 'info'
24
+ | 'danger'
25
+ | 'warning';
26
+ url?: string;
27
+ headers?: Record<string, string>;
28
+ onRecordingComplete?: (blob: Blob, url: string) => void;
29
+ };
30
+
31
+ let {
32
+ class: className,
33
+ name,
34
+ variant = 'primary',
35
+ url,
36
+ headers,
37
+ onRecordingComplete
38
+ }: Props = $props();
39
+
40
+ let mediaRecorder: MediaRecorder | null = null;
41
+ let audioContext: AudioContext | null = null;
42
+ let analyser: AnalyserNode | null = null;
43
+ let dataArray: Uint8Array | null = null;
44
+ let animationId: number | null = null;
45
+ let fileInput = $state<HTMLInputElement>();
46
+
47
+ let isRecording = $state(false);
48
+ let isPaused = $state(false);
49
+ let recordingTime = $state(0);
50
+ let waveformBars = $state<number[]>(Array(50).fill(0.2));
51
+ let audioChunks: Blob[] = [];
52
+ let timerInterval: number | null = null;
53
+ let isUploading = $state(false);
54
+ let isReviewing = $state(false);
55
+ let reviewAudioUrl: string | null = null;
56
+ let audioElement: HTMLAudioElement | null = null;
57
+ let isPlaying = $state(false);
58
+ let playbackTime = $state(0);
59
+ let playbackDuration = $state(0);
60
+ let playbackWaveform = $state<number[]>(Array(50).fill(0.2));
61
+
62
+ const BAR_COUNT = 50;
63
+
64
+ let baseClasses = $derived(cn('media', variant, className));
65
+
66
+ async function startRecording() {
67
+ try {
68
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
69
+
70
+ audioContext = new AudioContext();
71
+ const source = audioContext.createMediaStreamSource(stream);
72
+ analyser = audioContext.createAnalyser();
73
+ analyser.fftSize = 256;
74
+ source.connect(analyser);
75
+
76
+ const bufferLength = analyser.frequencyBinCount;
77
+ dataArray = new Uint8Array(bufferLength);
78
+
79
+ mediaRecorder = new MediaRecorder(stream);
80
+ audioChunks = [];
81
+
82
+ mediaRecorder.ondataavailable = (event) => {
83
+ if (event.data.size > 0) {
84
+ audioChunks.push(event.data);
85
+ }
86
+ };
87
+
88
+ mediaRecorder.onstop = async () => {
89
+ const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
90
+ reviewAudioUrl = URL.createObjectURL(audioBlob);
91
+
92
+ await analyzeRecordedAudio(audioBlob);
93
+
94
+ isReviewing = true;
95
+
96
+ stream.getTracks().forEach((track) => track.stop());
97
+ if (audioContext) {
98
+ audioContext.close();
99
+ }
100
+ };
101
+
102
+ mediaRecorder.start();
103
+ isRecording = true;
104
+ recordingTime = 0;
105
+
106
+ timerInterval = window.setInterval(() => {
107
+ recordingTime += 1;
108
+ }, 1000);
109
+
110
+ visualize();
111
+ } catch (error) {
112
+ console.error('Error accessing microphone:', error);
113
+ alert('No se pudo acceder al micrófono');
114
+ }
115
+ }
116
+
117
+ async function uploadAudio(blob: Blob) {
118
+ if (!url) return;
119
+
120
+ try {
121
+ isUploading = true;
122
+ const formData = new FormData();
123
+ formData.append(name, blob, `${name}.webm`);
124
+
125
+ const requestHeaders: HeadersInit = {
126
+ ...headers
127
+ };
128
+
129
+ const response = await fetch(url, {
130
+ method: 'POST',
131
+ headers: requestHeaders,
132
+ body: formData
133
+ });
134
+
135
+ if (!response.ok) {
136
+ throw new Error(`Upload failed: ${response.statusText}`);
137
+ }
138
+
139
+ console.log('Audio uploaded successfully');
140
+ } catch (error) {
141
+ console.error('Error uploading audio:', error);
142
+ alert('Error al subir el audio');
143
+ } finally {
144
+ isUploading = false;
145
+ }
146
+ }
147
+
148
+ function visualize() {
149
+ if (!analyser || !dataArray) return;
150
+
151
+ analyser.getByteFrequencyData(dataArray as any);
152
+ const newBars: number[] = [];
153
+ const step = Math.floor(dataArray.length / BAR_COUNT);
154
+
155
+ for (let i = 0; i < BAR_COUNT; i++) {
156
+ const index = i * step;
157
+ const value = dataArray[index] / 255;
158
+ newBars.push(value * 0.7 + 0.15);
159
+ }
160
+
161
+ waveformBars = newBars;
162
+
163
+ if (isRecording && !isPaused) {
164
+ animationId = requestAnimationFrame(visualize);
165
+ }
166
+ }
167
+
168
+ function pauseRecording() {
169
+ if (mediaRecorder && isRecording) {
170
+ mediaRecorder.pause();
171
+ isPaused = true;
172
+
173
+ if (timerInterval) {
174
+ clearInterval(timerInterval);
175
+ }
176
+
177
+ if (animationId) {
178
+ cancelAnimationFrame(animationId);
179
+ }
180
+ }
181
+ }
182
+
183
+ function resumeRecording() {
184
+ if (mediaRecorder && isPaused) {
185
+ mediaRecorder.resume();
186
+ isPaused = false;
187
+
188
+ timerInterval = window.setInterval(() => {
189
+ recordingTime += 1;
190
+ }, 1000);
191
+
192
+ visualize();
193
+ }
194
+ }
195
+
196
+ function stopRecording() {
197
+ if (mediaRecorder) {
198
+ mediaRecorder.stop();
199
+ isRecording = false;
200
+ isPaused = false;
201
+
202
+ if (timerInterval) {
203
+ clearInterval(timerInterval);
204
+ }
205
+
206
+ if (animationId) {
207
+ cancelAnimationFrame(animationId);
208
+ }
209
+
210
+ waveformBars = Array(50).fill(0.2);
211
+ }
212
+ }
213
+
214
+ function formatTime(seconds: number): string {
215
+ const mins = Math.floor(seconds / 60);
216
+ const secs = seconds % 60;
217
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
218
+ }
219
+
220
+ function handleToggleRecording() {
221
+ if (!isRecording) {
222
+ startRecording();
223
+ } else if (isPaused) {
224
+ resumeRecording();
225
+ } else {
226
+ pauseRecording();
227
+ }
228
+ }
229
+
230
+ async function confirmRecording() {
231
+ if (!reviewAudioUrl) return;
232
+
233
+ try {
234
+ const response = await fetch(reviewAudioUrl);
235
+ const audioBlob = await response.blob();
236
+
237
+ if (url) {
238
+ await uploadAudio(audioBlob);
239
+ } else {
240
+ const file = new File([audioBlob], `${name}.webm`, { type: 'audio/webm' });
241
+ const dataTransfer = new DataTransfer();
242
+ dataTransfer.items.add(file);
243
+ fileInput!.files = dataTransfer.files;
244
+
245
+ const event = new Event('change', { bubbles: true });
246
+ fileInput?.dispatchEvent(event);
247
+ }
248
+
249
+ if (onRecordingComplete) {
250
+ onRecordingComplete(audioBlob, reviewAudioUrl);
251
+ }
252
+
253
+ cleanup();
254
+ } catch (error) {
255
+ console.error('Error confirming recording:', error);
256
+ }
257
+ }
258
+
259
+ function discardRecording() {
260
+ cleanup();
261
+ }
262
+
263
+ function continueRecording() {
264
+ isReviewing = false;
265
+ isRecording = false;
266
+ isPaused = false;
267
+ }
268
+
269
+ async function analyzeRecordedAudio(blob: Blob) {
270
+ try {
271
+ const context = new AudioContext();
272
+ const arrayBuffer = await blob.arrayBuffer();
273
+ const audioBuffer = await context.decodeAudioData(arrayBuffer);
274
+
275
+ const rawData = audioBuffer.getChannelData(0);
276
+ const samples = BAR_COUNT;
277
+ const blockSize = Math.floor(rawData.length / samples);
278
+ const filteredData: number[] = [];
279
+
280
+ for (let i = 0; i < samples; i++) {
281
+ let sum = 0;
282
+ for (let j = 0; j < blockSize; j++) {
283
+ sum += Math.abs(rawData[i * blockSize + j]);
284
+ }
285
+ filteredData.push(sum / blockSize);
286
+ }
287
+
288
+ const max = Math.max(...filteredData);
289
+ playbackWaveform = filteredData.map((value) => (value / max) * 0.8 + 0.2);
290
+
291
+ await context.close();
292
+ } catch (error) {
293
+ console.error('Error analyzing audio:', error);
294
+ playbackWaveform = Array(50).fill(0.5);
295
+ }
296
+ }
297
+
298
+ function cleanup() {
299
+ if (reviewAudioUrl) {
300
+ URL.revokeObjectURL(reviewAudioUrl);
301
+ reviewAudioUrl = null;
302
+ }
303
+ if (audioElement) {
304
+ audioElement.pause();
305
+ audioElement = null;
306
+ }
307
+ isReviewing = false;
308
+ isRecording = false;
309
+ isPaused = false;
310
+ isPlaying = false;
311
+ recordingTime = 0;
312
+ playbackTime = 0;
313
+ playbackDuration = 0;
314
+ waveformBars = Array(50).fill(0.2);
315
+ playbackWaveform = Array(50).fill(0.2);
316
+ audioChunks = [];
317
+ }
318
+
319
+ function togglePlayback() {
320
+ if (!reviewAudioUrl) return;
321
+
322
+ if (!audioElement) {
323
+ audioElement = new Audio(reviewAudioUrl);
324
+
325
+ audioElement.addEventListener('loadedmetadata', () => {
326
+ playbackDuration = audioElement!.duration;
327
+ });
328
+
329
+ audioElement.addEventListener('timeupdate', () => {
330
+ if (audioElement) {
331
+ playbackTime = audioElement.currentTime;
332
+ }
333
+ });
334
+
335
+ audioElement.addEventListener('ended', () => {
336
+ isPlaying = false;
337
+ playbackTime = 0;
338
+ });
339
+ }
340
+
341
+ if (isPlaying) {
342
+ audioElement.pause();
343
+ isPlaying = false;
344
+ } else {
345
+ audioElement.play();
346
+ isPlaying = true;
347
+ }
348
+ }
349
+ </script>
350
+
351
+ {#if !url}
352
+ <input type="file" bind:this={fileInput} {name} accept="audio/*" class="hidden" />
353
+ {/if}
354
+
355
+ <div class={baseClasses}>
356
+ {#if isReviewing}
357
+ <Button onclick={togglePlayback} size="md" variant="ghost">
358
+ {#if isPlaying}
359
+ <Icon icon={Pause24RegularIcon} />
360
+ {:else}
361
+ <Icon icon={Play24RegularIcon} />
362
+ {/if}
363
+ </Button>
364
+
365
+ <div class="media-waveform">
366
+ <div class="media-bars">
367
+ {#each playbackWaveform as height, i}
368
+ {@const progress = playbackDuration > 0 ? playbackTime / playbackDuration : 0}
369
+ {@const barPosition = (i + 0.5) / playbackWaveform.length}
370
+ {@const isPlayed = barPosition <= progress}
371
+ <div class="media-bar" class:active={isPlayed} style="height: {height * 100}%"></div>
372
+ {/each}
373
+ </div>
374
+ </div>
375
+
376
+ <span class="media-time">{formatTime(recordingTime)}</span>
377
+
378
+ <div class="flex gap-2">
379
+ <Button onclick={discardRecording} size="md" variant="ghost">
380
+ <Icon icon={Delete24RegularIcon} />
381
+ </Button>
382
+ <Button onclick={continueRecording} size="md" variant="ghost">
383
+ <Icon icon={Record24RegularIcon} />
384
+ </Button>
385
+ <Button onclick={confirmRecording} size="md" variant="ghost">
386
+ <Icon icon={Checkmark24RegularIcon} />
387
+ </Button>
388
+ </div>
389
+ {:else if !isRecording}
390
+ <Button onclick={startRecording} size="md" variant="ghost">
391
+ <Icon icon={Record24RegularIcon} />
392
+ </Button>
393
+
394
+ <div class="media-waveform">
395
+ <div class="media-bars">
396
+ {#each waveformBars as height}
397
+ <div class="media-bar" style="height: {height * 100}%"></div>
398
+ {/each}
399
+ </div>
400
+ </div>
401
+
402
+ <span class="media-time">{formatTime(recordingTime)}</span>
403
+ {:else}
404
+ <Button onclick={handleToggleRecording} size="md" variant="ghost">
405
+ {#if isPaused}
406
+ <Icon icon={Play24RegularIcon} />
407
+ {:else}
408
+ <Icon icon={Pause24RegularIcon} />
409
+ {/if}
410
+ </Button>
411
+
412
+ <div class="media-waveform">
413
+ <div class="media-bars">
414
+ {#each waveformBars as height}
415
+ <div
416
+ class="media-bar"
417
+ class:recording={isRecording && !isPaused}
418
+ style="height: {height * 100}%"
419
+ ></div>
420
+ {/each}
421
+ </div>
422
+ </div>
423
+
424
+ <span class="media-time">{formatTime(recordingTime)}</span>
425
+
426
+ <Button onclick={stopRecording} size="md" variant="ghost" isLoading={isUploading}>
427
+ <Icon icon={RecordStop24RegularIcon} />
428
+ </Button>
429
+ {/if}
430
+ </div>
@@ -0,0 +1,11 @@
1
+ type Props = {
2
+ class?: string;
3
+ name: string;
4
+ variant?: 'primary' | 'secondary' | 'muted' | 'outlined' | 'ghost' | 'success' | 'info' | 'danger' | 'warning';
5
+ url?: string;
6
+ headers?: Record<string, string>;
7
+ onRecordingComplete?: (blob: Blob, url: string) => void;
8
+ };
9
+ declare const Record: import("svelte").Component<Props, {}, "">;
10
+ type Record = ReturnType<typeof Record>;
11
+ export default Record;
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { MoonStarsLinearIcon, Sun2LinearIcon } from '../icons/index.js';
3
+ import { Button, Icon } from '../index.js';
4
+ import { theme } from '../stores/theme.svelte.js';
5
+
6
+ type Props = {
7
+ variant?: 'primary' | 'secondary' | 'muted' | 'ghost';
8
+ size?: 'sm' | 'md' | 'lg';
9
+ class?: string;
10
+ };
11
+
12
+ let { variant = 'ghost', size = 'md', class: className }: Props = $props();
13
+ </script>
14
+
15
+ <Button onclick={theme.toggleTheme} {size} {variant} class={className} isIcon>
16
+ {#if theme.isDark}
17
+ <Icon icon={Sun2LinearIcon} />
18
+ {:else}
19
+ <Icon icon={MoonStarsLinearIcon} />
20
+ {/if}
21
+ </Button>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ variant?: 'primary' | 'secondary' | 'muted' | 'ghost';
3
+ size?: 'sm' | 'md' | 'lg';
4
+ class?: string;
5
+ };
6
+ declare const ToggleTheme: import("svelte").Component<Props, {}, "">;
7
+ type ToggleTheme = ReturnType<typeof ToggleTheme>;
8
+ export default ToggleTheme;
@@ -0,0 +1,222 @@
1
+ <script lang="ts">
2
+ import {
3
+ MaximizeSquareMinimalisticLinearIcon,
4
+ Pause24RegularIcon,
5
+ PictureInPicture24RegularIcon,
6
+ Play24RegularIcon,
7
+ Speaker24RegularIcon,
8
+ SpeakerMute24RegularIcon
9
+ } from '../icons/index.js';
10
+ import { Icon, Slider } from '../index.js';
11
+ import { cn } from '../utils/class-names.js';
12
+ import Hls from 'hls.js';
13
+
14
+ type Props = {
15
+ src: string;
16
+ autoplay?: boolean;
17
+ poster?: string;
18
+ aspect?: 'horizontal' | 'vertical' | 'square';
19
+ class?: string;
20
+ };
21
+
22
+ let { src, poster, autoplay, aspect = 'horizontal', class: className }: Props = $props();
23
+
24
+ let videoElement: HTMLVideoElement | null = $state(null);
25
+
26
+ let showControls = $state(false);
27
+ let showVolume = $state(false);
28
+ let videoParams = $state({
29
+ src,
30
+ time: 0,
31
+ duration: 0,
32
+ formattedTime: '00:00',
33
+ formattedDuration: '00:00',
34
+ controls: true,
35
+ muted: false,
36
+ paused: false,
37
+ volume: 1
38
+ });
39
+
40
+ const setSource = () => {
41
+ if (src.includes('.m3u8')) {
42
+ if (Hls.isSupported() && videoElement) {
43
+ const hls = new Hls();
44
+ hls.loadSource(src);
45
+ hls.attachMedia(videoElement);
46
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
47
+ if (autoplay) {
48
+ videoElement?.play();
49
+ }
50
+ });
51
+ } else if (videoElement?.canPlayType('application/vnd.apple.mpegurl')) {
52
+ videoElement.src = src;
53
+ }
54
+ } else {
55
+ if (!videoElement) return;
56
+ videoElement.src = src;
57
+ }
58
+ };
59
+
60
+ const handleMouseEnter = () => {
61
+ if (showControls) return;
62
+ showControls = true;
63
+ setTimeout(() => {
64
+ showControls = false;
65
+ }, 5000);
66
+ };
67
+
68
+ const togglePlay = () => {
69
+ if (videoElement?.paused) {
70
+ videoElement.play();
71
+ } else {
72
+ videoElement?.pause();
73
+ }
74
+ };
75
+ const toggleMute = () => {
76
+ if (!videoElement) return;
77
+ videoElement.muted = !videoElement.muted;
78
+ localStorage.setItem('video-muted', videoElement.muted.toString());
79
+ };
80
+
81
+ const formatTime = (seconds: number): string => {
82
+ const mins = Math.floor(seconds / 60);
83
+ const secs = Math.floor(seconds % 60);
84
+ return `${mins}:${secs < 10 ? '0' + secs : secs}`;
85
+ };
86
+
87
+ const handleToggleMaximize = () => {
88
+ const containerElement = document.querySelector('.video');
89
+
90
+ if (!document.fullscreenElement) {
91
+ if (containerElement?.requestFullscreen) {
92
+ containerElement.requestFullscreen();
93
+ }
94
+ } else {
95
+ if (document.exitFullscreen) {
96
+ document.exitFullscreen();
97
+ }
98
+ }
99
+ };
100
+
101
+ const handleTogglePip = async () => {
102
+ if (!document.pictureInPictureEnabled) return;
103
+
104
+ try {
105
+ if (document.pictureInPictureElement === videoElement) {
106
+ await document.exitPictureInPicture();
107
+ } else {
108
+ await videoElement?.requestPictureInPicture();
109
+ }
110
+ } catch (error) {
111
+ console.error('PiP error:', error);
112
+ }
113
+ };
114
+
115
+ $effect(() => {
116
+ if (localStorage.getItem('video-muted') === 'true') {
117
+ videoParams.muted = true;
118
+ }
119
+ });
120
+
121
+ $effect(() => {
122
+ if (localStorage.getItem('video-volume')) {
123
+ videoParams.volume = parseFloat(localStorage.getItem('video-volume') || '1');
124
+ }
125
+ });
126
+
127
+ $effect(() => {
128
+ videoParams.formattedTime = formatTime(videoParams.time);
129
+ videoParams.formattedDuration = formatTime(videoParams.duration);
130
+ });
131
+
132
+ $effect(() => {
133
+ setSource();
134
+ });
135
+ </script>
136
+
137
+ <svelte:window onmousemove={handleMouseEnter} />
138
+
139
+ <div class={cn('video', aspect, className)}>
140
+ <!-- svelte-ignore component_name_lowercase -->
141
+ <video
142
+ bind:this={videoElement}
143
+ bind:currentTime={videoParams.time}
144
+ bind:duration={videoParams.duration}
145
+ bind:paused={videoParams.paused}
146
+ bind:muted={videoParams.muted}
147
+ bind:volume={videoParams.volume}
148
+ class="rounded-lg"><track kind="captions" /></video
149
+ >
150
+ {#if poster && videoParams.paused && videoElement.currentTime === 0}
151
+ <img src={poster} class="video-poster rounded-md" alt="poster" />
152
+ {/if}
153
+
154
+ <button onclick={togglePlay} aria-label="Play" class="video-control-play"></button>
155
+ <div class="video-controls">
156
+ <!-- svelte-ignore a11y_consider_explicit_label -->
157
+ <div class={cn('video-control-actions', showControls ? 'visible' : 'invisible')}>
158
+ <div class="video-actions-start">
159
+ <button class="video-btn" onclick={togglePlay}>
160
+ {#if videoParams.paused}
161
+ <Icon icon={Play24RegularIcon} class="video-btn-icon" />
162
+ {:else}
163
+ <Icon icon={Pause24RegularIcon} class="video-btn-icon" />
164
+ {/if}
165
+ </button>
166
+ <div class="video-btn">
167
+ <span class="video-duration-info"
168
+ >{videoParams.formattedTime} / {videoParams.formattedDuration}</span
169
+ >
170
+ </div>
171
+ </div>
172
+ <div class="video-actions-end">
173
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
174
+ <div
175
+ class="video-volume-wrapper"
176
+ onmouseenter={() => (showVolume = true)}
177
+ onmouseleave={() => (showVolume = false)}
178
+ >
179
+ {#if !videoParams.muted && showVolume}
180
+ <div class="video-volume">
181
+ <Slider
182
+ bind:value={videoParams.volume}
183
+ onchange={(value: number) => {
184
+ localStorage.setItem('video-volume', String(value));
185
+ }}
186
+ min={0}
187
+ max={1}
188
+ step={0.1}
189
+ size="sm"
190
+ name="video-volume"
191
+ hideLabel
192
+ />
193
+ </div>
194
+ {/if}
195
+ <button class="video-btn" onclick={toggleMute}>
196
+ {#if videoParams.muted}
197
+ <Icon icon={Speaker24RegularIcon} class="video-btn-icon" />
198
+ {:else}
199
+ <Icon icon={SpeakerMute24RegularIcon} class="video-btn-icon" />
200
+ {/if}
201
+ </button>
202
+ </div>
203
+ <button class="video-btn" onclick={handleTogglePip}>
204
+ <Icon icon={PictureInPicture24RegularIcon} class="video-btn-icon" />
205
+ </button>
206
+ <button class="video-btn" onclick={handleToggleMaximize}>
207
+ <Icon icon={MaximizeSquareMinimalisticLinearIcon} class="video-btn-icon" />
208
+ </button>
209
+ </div>
210
+ </div>
211
+ <div class={cn('video-control-progress', showControls ? 'visible' : 'invisible')}>
212
+ <Slider
213
+ min={0}
214
+ size="sm"
215
+ max={videoParams.duration}
216
+ bind:value={videoParams.time}
217
+ name="video-time"
218
+ hideLabel
219
+ />
220
+ </div>
221
+ </div>
222
+ </div>
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ src: string;
3
+ autoplay?: boolean;
4
+ poster?: string;
5
+ aspect?: 'horizontal' | 'vertical' | 'square';
6
+ class?: string;
7
+ };
8
+ declare const Video: import("svelte").Component<Props, {}, "">;
9
+ type Video = ReturnType<typeof Video>;
10
+ export default Video;