trix-ui 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trix-ui",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "Lite UI CLI, registry tooling, and templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,11 +27,14 @@ export type MusicPlayerCardProps = {
27
27
  /** Optional playlist */
28
28
  tracks?: MusicTrack[];
29
29
 
30
- /** Optional File object for local playback */
31
- audioFile?: File;
32
-
33
- /** Initial index when using tracks (default 0) */
34
- initialTrackIndex?: number;
30
+ /** Optional File object for local playback */
31
+ audioFile?: File;
32
+
33
+ /** Optional list of local files for playlist playback */
34
+ audioFiles?: File[];
35
+
36
+ /** Initial index when using tracks (default 0) */
37
+ initialTrackIndex?: number;
35
38
 
36
39
  /** Auto play on load (default false) */
37
40
  autoPlay?: boolean;
@@ -52,9 +55,10 @@ export type MusicPlayerCardProps = {
52
55
 
53
56
  onShuffleChange?: (enabled: boolean) => void;
54
57
  onRepeatChange?: (enabled: boolean) => void;
55
- onPlayStateChange?: (playing: boolean) => void;
56
- onTrackChange?: (track: MusicTrack, index: number) => void;
57
- onFileSelect?: (file: File) => void;
58
+ onPlayStateChange?: (playing: boolean) => void;
59
+ onTrackChange?: (track: MusicTrack, index: number) => void;
60
+ onFileSelect?: (file: File) => void;
61
+ onFilesSelect?: (files: File[]) => void;
58
62
 
59
63
  /** Toggle states */
60
64
  isPlaying?: boolean;
@@ -79,32 +83,38 @@ export type MusicPlayerCardProps = {
79
83
 
80
84
  const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
81
85
 
82
- function formatTime(value: number): string {
83
- if (!Number.isFinite(value) || value <= 0) return "0:00";
84
- const minutes = Math.floor(value / 60);
85
- const seconds = Math.floor(value % 60);
86
- return `${minutes}:${seconds.toString().padStart(2, "0")}`;
87
- }
88
-
89
- function useObjectUrl(file?: File): string | null {
90
- const [url, setUrl] = React.useState<string | null>(null);
91
-
92
- React.useEffect(() => {
93
- if (!file) {
94
- setUrl(null);
95
- return undefined;
96
- }
97
-
98
- const nextUrl = URL.createObjectURL(file);
99
- setUrl(nextUrl);
100
-
101
- return () => {
102
- URL.revokeObjectURL(nextUrl);
103
- };
104
- }, [file]);
105
-
106
- return url;
107
- }
86
+ function formatTime(value: number): string {
87
+ if (!Number.isFinite(value) || value <= 0) return "0:00";
88
+ const minutes = Math.floor(value / 60);
89
+ const seconds = Math.floor(value % 60);
90
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
91
+ }
92
+
93
+ function useObjectUrls(files: File[]): string[] {
94
+ const [urls, setUrls] = React.useState<string[]>([]);
95
+
96
+ React.useEffect(() => {
97
+ if (!files.length) {
98
+ setUrls([]);
99
+ return undefined;
100
+ }
101
+
102
+ const nextUrls = files.map((file) => URL.createObjectURL(file));
103
+ setUrls(nextUrls);
104
+
105
+ return () => {
106
+ nextUrls.forEach((url) => URL.revokeObjectURL(url));
107
+ };
108
+ }, [files]);
109
+
110
+ return urls;
111
+ }
112
+
113
+ function getFileTitle(file: File): string {
114
+ const name = file.name?.trim() || "Untitled";
115
+ const trimmed = name.replace(/\.[^/.]+$/, "");
116
+ return trimmed || name;
117
+ }
108
118
 
109
119
  const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProps>(
110
120
  (
@@ -113,12 +123,13 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
113
123
  title,
114
124
  subtitle = "Unknown Artist",
115
125
 
116
- audioSrc,
117
- tracks,
118
- audioFile,
119
- initialTrackIndex = 0,
120
- autoPlay = false,
121
- showFileInput = false,
126
+ audioSrc,
127
+ tracks,
128
+ audioFile,
129
+ audioFiles,
130
+ initialTrackIndex = 0,
131
+ autoPlay = false,
132
+ showFileInput = false,
122
133
 
123
134
  progress,
124
135
  currentTime,
@@ -131,10 +142,11 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
131
142
  onRepeat,
132
143
 
133
144
  onShuffleChange,
134
- onRepeatChange,
135
- onPlayStateChange,
136
- onTrackChange,
137
- onFileSelect,
145
+ onRepeatChange,
146
+ onPlayStateChange,
147
+ onTrackChange,
148
+ onFileSelect,
149
+ onFilesSelect,
138
150
 
139
151
  isPlaying,
140
152
  isShuffleOn,
@@ -159,45 +171,63 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
159
171
  ref
160
172
  ) => {
161
173
  const [internalPlaying, setInternalPlaying] = React.useState(false);
162
- const [internalShuffle, setInternalShuffle] = React.useState(false);
163
- const [internalRepeat, setInternalRepeat] = React.useState(false);
164
- const [trackIndex, setTrackIndex] = React.useState(
165
- clamp(initialTrackIndex, 0, Math.max((tracks?.length ?? 1) - 1, 0))
166
- );
167
- const [uploadedFile, setUploadedFile] = React.useState<File | null>(null);
174
+ const [internalShuffle, setInternalShuffle] = React.useState(false);
175
+ const [internalRepeat, setInternalRepeat] = React.useState(false);
176
+ const [trackIndex, setTrackIndex] = React.useState(
177
+ clamp(initialTrackIndex, 0, Math.max((tracks?.length ?? 1) - 1, 0))
178
+ );
179
+ const [uploadedFiles, setUploadedFiles] = React.useState<File[]>([]);
168
180
  const [currentTimeSec, setCurrentTimeSec] = React.useState(0);
169
181
  const [durationSec, setDurationSec] = React.useState(0);
170
182
  const [playbackError, setPlaybackError] = React.useState<string | null>(null);
171
183
  const audioRef = React.useRef<HTMLAudioElement | null>(null);
172
184
  const shuffleHistoryRef = React.useRef<number[]>([]);
173
185
 
174
- const isShuffle = isShuffleOn ?? internalShuffle;
175
- const isRepeat = isRepeatOn ?? internalRepeat;
176
- const playing = isPlaying ?? internalPlaying;
177
-
178
- const playlist = tracks?.length ? tracks : [];
179
- const fallbackTrack: MusicTrack = {
180
- src: audioSrc ?? "",
181
- title,
182
- subtitle,
183
- imageSrc,
184
- };
185
-
186
- const activeTrack = playlist.length
187
- ? playlist[clamp(trackIndex, 0, playlist.length - 1)]
188
- : fallbackTrack;
189
-
190
- const propFileUrl = useObjectUrl(audioFile);
191
- const uploadedFileUrl = useObjectUrl(uploadedFile ?? undefined);
192
- const resolvedSrc = propFileUrl ?? uploadedFileUrl ?? activeTrack.src;
186
+ const isShuffle = isShuffleOn ?? internalShuffle;
187
+ const isRepeat = isRepeatOn ?? internalRepeat;
188
+ const playing = isPlaying ?? internalPlaying;
189
+
190
+ const fileList = React.useMemo<File[]>(() => {
191
+ if (audioFiles?.length) return audioFiles;
192
+ if (uploadedFiles.length) return uploadedFiles;
193
+ if (audioFile) return [audioFile];
194
+ return [];
195
+ }, [audioFile, audioFiles, uploadedFiles]);
196
+
197
+ const fileUrls = useObjectUrls(fileList);
198
+ const fileTracks = React.useMemo<MusicTrack[]>(() => {
199
+ if (!fileList.length) return [];
200
+ return fileList.map((file, index) => ({
201
+ src: fileUrls[index] ?? "",
202
+ title: getFileTitle(file),
203
+ subtitle: "Local file",
204
+ imageSrc,
205
+ }));
206
+ }, [fileList, fileUrls, imageSrc]);
207
+
208
+ const playlist = fileTracks.length ? fileTracks : tracks?.length ? tracks : [];
209
+ const fallbackTrack: MusicTrack = {
210
+ src: audioSrc ?? "",
211
+ title,
212
+ subtitle,
213
+ imageSrc,
214
+ };
215
+
216
+ const activeTrack = playlist.length
217
+ ? playlist[clamp(trackIndex, 0, playlist.length - 1)]
218
+ : fallbackTrack;
219
+
220
+ const resolvedSrc = activeTrack.src;
193
221
  const isPlayable = Boolean(resolvedSrc);
194
222
  const hasExplicitSource =
195
223
  Boolean(audioSrc) ||
196
224
  Boolean(audioFile) ||
197
- Boolean(uploadedFile) ||
225
+ Boolean(fileList.length) ||
198
226
  Boolean(playlist.length && activeTrack.src);
199
227
  const missingSourceMessage =
200
- !resolvedSrc && hasExplicitSource ? "Audio file not found." : null;
228
+ !resolvedSrc && hasExplicitSource && fileList.length === 0
229
+ ? "Audio file not found."
230
+ : null;
201
231
 
202
232
  const derivedProgress =
203
233
  durationSec > 0 ? clamp((currentTimeSec / durationSec) * 100, 0, 100) : 0;
@@ -412,16 +442,19 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
412
442
  onPlayStateChange?.(false);
413
443
  };
414
444
 
415
- const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
416
- const file = event.target.files?.[0];
417
- if (!file) return;
418
- setUploadedFile(file);
419
- onFileSelect?.(file);
420
- if (isPlaying === undefined) {
421
- setInternalPlaying(true);
422
- }
423
- onPlayStateChange?.(true);
424
- };
445
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
446
+ const files = Array.from(event.target.files ?? []);
447
+ if (!files.length) return;
448
+ setUploadedFiles(files);
449
+ setTrackIndex(0);
450
+ onFileSelect?.(files[0]);
451
+ onFilesSelect?.(files);
452
+ if (isPlaying === undefined) {
453
+ setInternalPlaying(true);
454
+ }
455
+ onPlayStateChange?.(true);
456
+ event.currentTarget.value = "";
457
+ };
425
458
 
426
459
  return (
427
460
  <div
@@ -479,12 +512,13 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
479
512
  {showFileInput ? (
480
513
  <label className="mb-4 inline-flex cursor-pointer items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-stone-400">
481
514
  <span>Load audio</span>
482
- <input
483
- type="file"
484
- accept="audio/*"
485
- className="sr-only"
486
- onChange={handleFileChange}
487
- />
515
+ <input
516
+ type="file"
517
+ accept="audio/*"
518
+ multiple
519
+ className="sr-only"
520
+ onChange={handleFileChange}
521
+ />
488
522
  </label>
489
523
  ) : null}
490
524