mikuru 1.0.31 → 1.0.32

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/CHANGELOG.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## 1.0.31 - 2026-05-16
3
+ ## 1.0.32 - 2026-05-16
4
4
 
5
+ - Added `controls` and `live` props to `MikuruVideoPlayer` and `MikuruAudioPlayer` so callers can choose visible controls and render live-stream UI without seek controls.
6
+ - Removed the `MikuruVideoPlayer` stop control so video playback uses the same play/pause-only primary transport as the center control.
7
+ - Updated `MikuruAudioPlayer` controls to use icon buttons for play, skip, mute, and volume-facing actions, matching the video player control style.
8
+ - Added timed auto-dismiss to `MikuruToast` with stack-level and per-toast `duration` controls.
5
9
  - Exported `MikuruVideoPlayer` media events so parent components can listen for playback, timing, seeking, volume, and playback-rate changes with typed media state payloads.
6
10
  - Exported matching `MikuruAudioPlayer` media events and documented the shared media player event payload.
7
11
 
package/README.md CHANGED
@@ -270,12 +270,12 @@ npm run dev:mikuru-vue-like
270
270
 
271
271
  The package also includes original Mikuru components:
272
272
 
273
- - `MikuruVideoPlayer.mikuru`: overlay video controls, div-based seeking, volume/mute, playback rate, stop, and fullscreen controls.
274
- - `MikuruAudioPlayer.mikuru`: audio playback with seeking, skip controls, volume, and mute.
273
+ - `MikuruVideoPlayer.mikuru`: overlay video controls, configurable control visibility, live mode, div-based seeking, volume/mute, playback rate, and fullscreen controls.
274
+ - `MikuruAudioPlayer.mikuru`: audio playback with configurable control visibility, live mode, seeking, skip controls, volume, and mute.
275
275
  - `MikuruImageViewer.mikuru`: image zoom, pan, rotate, reset, and fullscreen controls.
276
276
  - `MikuruModal.mikuru`: accessible modal shell with backdrop, Escape close, slots, and close events.
277
277
  - `MikuruCarousel.mikuru`: image carousel with arrows, dots, keyboard navigation, and optional autoplay.
278
- - `MikuruToast.mikuru`: fixed notification stack with dismiss events and tone variants.
278
+ - `MikuruToast.mikuru`: fixed notification stack with timed auto-dismiss, dismiss events, and tone variants.
279
279
  - `MikuruDropdown.mikuru`: menu button with outside-click close, Escape handling, and select events.
280
280
  - `MikuruToolTip.mikuru`: hover/focus tooltip with configurable placement.
281
281
  - `MikuruProgress.mikuru`: determinate and indeterminate progress indicator.
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section class="mikuru-audio" :class="{ 'is-playing': isPlaying }" :aria-label="title">
2
+ <section class="mikuru-audio" :class="{ 'is-playing': isPlaying, 'is-live': isLive }" :aria-label="title">
3
3
  <audio
4
4
  ref="mediaEl"
5
5
  :src="src"
@@ -22,28 +22,33 @@
22
22
  <div class="body">
23
23
  <div class="identity">
24
24
  <strong>{{ title }}</strong>
25
- <span>{{ artist }}</span>
25
+ <span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
26
26
  </div>
27
27
 
28
- <div class="timeline">
29
- <span>{{ formatTime(currentTime) }}</span>
30
- <input type="range" min="0" :max="safeDuration" step="0.1" :value="currentTime" @input="seek" />
31
- <span>{{ formatTime(duration) }}</span>
28
+ <div m-if="showTimeline" class="timeline" :class="timelineClass">
29
+ <span m-if="showTimeControl">{{ formatTime(currentTime) }}</span>
30
+ <input m-if="showSeekControl" type="range" min="0" :max="safeDuration" step="0.1" :value="currentTime" @input="seek" />
31
+ <span m-if="showTimeControl">{{ formatTime(duration) }}</span>
32
32
  </div>
33
33
 
34
- <div class="controls">
35
- <button type="button" @click="skipBackward">-10</button>
36
- <button class="primary" type="button" @click="togglePlayback" :aria-label="playLabel">
37
- <span>{{ playText }}</span>
34
+ <div m-if="showControls" class="controls">
35
+ <button m-if="showSkipControl" class="icon-button" type="button" @click="skipBackward" aria-label="Back 10 seconds">
36
+ <span class="fa-icon icon-backward" aria-hidden="true"></span>
38
37
  </button>
39
- <button type="button" @click="skipForward">+10</button>
40
- <button type="button" @click="toggleMute" :aria-label="muteLabel">
41
- <span>Sound</span>
38
+ <button m-if="showPlayControl" class="icon-button primary" type="button" @click="togglePlayback" :aria-label="playLabel">
39
+ <span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
42
40
  </button>
43
- <label class="volume">
41
+ <button m-if="showSkipControl" class="icon-button" type="button" @click="skipForward" aria-label="Forward 10 seconds">
42
+ <span class="fa-icon icon-forward" aria-hidden="true"></span>
43
+ </button>
44
+ <button m-if="showMuteControl" class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
45
+ <span class="fa-icon" :class="muteIconClass" aria-hidden="true"></span>
46
+ </button>
47
+ <label m-if="showVolumeControl" class="volume">
44
48
  <span>Volume</span>
45
49
  <input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
46
50
  </label>
51
+ <span m-if="isLive" class="live-clock">LIVE</span>
47
52
  </div>
48
53
  </div>
49
54
  </section>
@@ -56,12 +61,16 @@ const {
56
61
  src,
57
62
  title = "Mikuru Audio",
58
63
  artist = "Original player component",
59
- preload = "metadata"
64
+ preload = "metadata",
65
+ controls,
66
+ live = false
60
67
  } = defineProps({
61
68
  src: String,
62
69
  title: String,
63
70
  artist: String,
64
- preload: String
71
+ preload: String,
72
+ controls: Array,
73
+ live: Boolean
65
74
  });
66
75
 
67
76
  const emit = defineEmits([
@@ -82,10 +91,37 @@ const duration = ref(0);
82
91
  const volume = ref(0.75);
83
92
  const muted = ref(false);
84
93
  const isPlaying = ref(false);
94
+ const allControls = ["play", "seek", "time", "skip", "mute", "volume"];
95
+ const liveHiddenControls = ["seek", "time", "skip"];
96
+ const activeControls = computed(() => {
97
+ const configuredControls = Array.isArray(controls.value) ? controls.value : allControls;
98
+ return new Set(configuredControls);
99
+ });
100
+ const isLive = computed(() => live.value === true);
85
101
  const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
86
102
  const playLabel = computed(() => isPlaying.value ? "Pause audio" : "Play audio");
87
103
  const muteLabel = computed(() => muted.value ? "Unmute audio" : "Mute audio");
88
- const playText = computed(() => isPlaying.value ? "Pause" : "Play");
104
+ const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
105
+ const muteIconClass = computed(() => muted.value ? "icon-mute" : "icon-volume");
106
+ const statusText = computed(() => isLive.value ? "LIVE" : artist.value);
107
+ const showPlayControl = computed(() => hasControl("play"));
108
+ const showSeekControl = computed(() => hasControl("seek"));
109
+ const showTimeControl = computed(() => hasControl("time"));
110
+ const showSkipControl = computed(() => hasControl("skip"));
111
+ const showMuteControl = computed(() => hasControl("mute"));
112
+ const showVolumeControl = computed(() => hasControl("volume"));
113
+ const showTimeline = computed(() => showSeekControl.value || showTimeControl.value);
114
+ const timelineClass = computed(() => ({
115
+ "timeline-seek-only": showSeekControl.value && !showTimeControl.value,
116
+ "timeline-time-only": showTimeControl.value && !showSeekControl.value
117
+ }));
118
+ const showControls = computed(() => (
119
+ showPlayControl.value ||
120
+ showSkipControl.value ||
121
+ showMuteControl.value ||
122
+ showVolumeControl.value ||
123
+ isLive.value
124
+ ));
89
125
  const initials = computed(() => {
90
126
  const words = title.value.split(" ").filter(Boolean);
91
127
  return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
@@ -114,6 +150,11 @@ function shouldIgnoreMediaEvent() {
114
150
  return performance.now() < ignoreMediaEventsUntil;
115
151
  }
116
152
 
153
+ function hasControl(name) {
154
+ if (isLive.value && liveHiddenControls.includes(name)) return false;
155
+ return activeControls.value.has(name);
156
+ }
157
+
117
158
  function syncMedia() {
118
159
  if (isDisposed || shouldIgnoreMediaEvent()) return;
119
160
  const media = getMedia();
@@ -122,6 +163,14 @@ function syncMedia() {
122
163
  duration.value = Number.isFinite(media.duration) ? media.duration : 0;
123
164
  }
124
165
 
166
+ function syncVolumeState() {
167
+ if (isDisposed) return;
168
+ const media = getMedia();
169
+ if (!media) return;
170
+ volume.value = media.volume;
171
+ muted.value = media.muted;
172
+ }
173
+
125
174
  function createMediaPayload(event) {
126
175
  const media = getMedia();
127
176
  if (!media) return null;
@@ -166,6 +215,7 @@ function handleSeeked(event) {
166
215
 
167
216
  function handleVolumeChange(event) {
168
217
  syncMedia();
218
+ syncVolumeState();
169
219
  emitMediaPayload(event, (payload) => emit("volumechange", payload));
170
220
  }
171
221
 
@@ -235,19 +285,19 @@ function setVolume(event) {
235
285
  if (!media) return;
236
286
  media.volume = nextVolume;
237
287
  media.muted = nextVolume === 0;
288
+ volume.value = nextVolume;
289
+ muted.value = media.muted;
238
290
  }, 0);
239
291
  }
240
292
 
241
- function toggleMute(event) {
293
+ function toggleMute() {
242
294
  if (isDisposed) return;
243
- const button = event.currentTarget;
244
295
  window.setTimeout(() => {
245
296
  if (isDisposed) return;
246
297
  const media = getMedia();
247
298
  if (!media) return;
248
299
  media.muted = !media.muted;
249
- button.textContent = media.muted ? "Muted" : "Sound";
250
- button.setAttribute("aria-label", media.muted ? "Unmute audio" : "Mute audio");
300
+ muted.value = media.muted;
251
301
  }, 0);
252
302
  }
253
303
 
@@ -318,6 +368,32 @@ function formatTime(seconds) {
318
368
  font-size: 0.9rem;
319
369
  }
320
370
 
371
+ .live-badge,
372
+ .live-clock {
373
+ color: #ffffff;
374
+ font-size: 0.78rem;
375
+ font-weight: 800;
376
+ letter-spacing: 0;
377
+ }
378
+
379
+ .live-badge {
380
+ display: inline-flex;
381
+ align-items: center;
382
+ width: max-content;
383
+ padding: 4px 8px;
384
+ border-radius: 999px;
385
+ background: #dc2626;
386
+ }
387
+
388
+ .live-clock {
389
+ display: inline-flex;
390
+ align-items: center;
391
+ min-height: 24px;
392
+ padding: 0 8px;
393
+ border-radius: 999px;
394
+ background: #dc2626;
395
+ }
396
+
321
397
  .timeline {
322
398
  display: grid;
323
399
  grid-template-columns: 42px minmax(0, 1fr) 42px;
@@ -325,6 +401,15 @@ function formatTime(seconds) {
325
401
  align-items: center;
326
402
  }
327
403
 
404
+ .timeline-seek-only {
405
+ grid-template-columns: minmax(0, 1fr);
406
+ }
407
+
408
+ .timeline-time-only {
409
+ grid-template-columns: repeat(2, 42px);
410
+ justify-content: space-between;
411
+ }
412
+
328
413
  .timeline span:last-child {
329
414
  text-align: right;
330
415
  }
@@ -342,6 +427,14 @@ function formatTime(seconds) {
342
427
  gap: 8px;
343
428
  }
344
429
 
430
+ .volume span {
431
+ position: absolute;
432
+ width: 1px;
433
+ height: 1px;
434
+ overflow: hidden;
435
+ clip: rect(0 0 0 0);
436
+ }
437
+
345
438
  input[type="range"] {
346
439
  accent-color: #0f766e;
347
440
  }
@@ -367,6 +460,49 @@ button:hover {
367
460
  background: #0f766e;
368
461
  }
369
462
 
463
+ .icon-button {
464
+ display: inline-grid;
465
+ place-items: center;
466
+ width: 38px;
467
+ padding: 0;
468
+ }
469
+
470
+ .fa-icon {
471
+ display: block;
472
+ width: 18px;
473
+ height: 18px;
474
+ background: currentColor;
475
+ filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.28));
476
+ mask-position: center;
477
+ mask-repeat: no-repeat;
478
+ mask-size: contain;
479
+ }
480
+
481
+ .icon-play {
482
+ transform: translateX(1px);
483
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M187.2 100.9C174.8 94.1 159.8 94.4 147.6 101.6C135.4 108.8 128 121.9 128 136L128 504C128 518.1 135.5 531.2 147.6 538.4C159.7 545.6 174.8 545.9 187.2 539.1L523.2 355.1C536 348.1 544 334.6 544 320C544 305.4 536 291.9 523.2 284.9L187.2 100.9z"/></svg>');
484
+ }
485
+
486
+ .icon-pause {
487
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M176 96C149.5 96 128 117.5 128 144L128 496C128 522.5 149.5 544 176 544L240 544C266.5 544 288 522.5 288 496L288 144C288 117.5 266.5 96 240 96L176 96zM400 96C373.5 96 352 117.5 352 144L352 496C352 522.5 373.5 544 400 544L464 544C490.5 544 512 522.5 512 496L512 144C512 117.5 490.5 96 464 96L400 96z"/></svg>');
488
+ }
489
+
490
+ .icon-backward {
491
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M288 128L96 320L288 512L288 128zM544 128L352 320L544 512L544 128z"/></svg>');
492
+ }
493
+
494
+ .icon-forward {
495
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M96 128L288 320L96 512L96 128zM352 128L544 320L352 512L352 128z"/></svg>');
496
+ }
497
+
498
+ .icon-volume {
499
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M112 416L160 416L294.1 535.2C300.5 540.9 308.7 544 317.2 544C336.4 544 352 528.4 352 509.2L352 130.8C352 111.6 336.4 96 317.2 96C308.7 96 300.5 99.1 294.1 104.8L160 224L112 224C85.5 224 64 245.5 64 272L64 368C64 394.5 85.5 416 112 416zM505.1 171C494.8 162.6 479.7 164.2 471.3 174.5C462.9 184.8 464.5 199.9 474.8 208.3C507.3 234.7 528 274.9 528 320C528 365.1 507.3 405.3 474.8 431.8C464.5 440.2 463 455.3 471.3 465.6C479.6 475.9 494.8 477.4 505.1 469.1C548.3 433.9 576 380.2 576 320.1C576 260 548.3 206.3 505.1 171.1zM444.6 245.5C434.3 237.1 419.2 238.7 410.8 249C402.4 259.3 404 274.4 414.3 282.8C425.1 291.6 432 305 432 320C432 335 425.1 348.4 414.3 357.3C404 365.7 402.5 380.8 410.8 391.1C419.1 401.4 434.3 402.9 444.6 394.6C466.1 376.9 480 350.1 480 320C480 289.9 466.1 263.1 444.5 245.5z"/></svg>');
500
+ }
501
+
502
+ .icon-mute {
503
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M80 416L128 416L262.1 535.2C268.5 540.9 276.7 544 285.2 544C304.4 544 320 528.4 320 509.2L320 130.8C320 111.6 304.4 96 285.2 96C276.7 96 268.5 99.1 262.1 104.8L128 224L80 224C53.5 224 32 245.5 32 272L32 368C32 394.5 53.5 416 80 416zM399 239C389.6 248.4 389.6 263.6 399 272.9L446 319.9L399 366.9C389.6 376.3 389.6 391.5 399 400.8C408.4 410.1 423.6 410.2 432.9 400.8L479.9 353.8L526.9 400.8C536.3 410.2 551.5 410.2 560.8 400.8C570.1 391.4 570.2 376.2 560.8 366.9L513.8 319.9L560.8 272.9C570.2 263.5 570.2 248.3 560.8 239C551.4 229.7 536.2 229.6 526.9 239L479.9 286L432.9 239C423.5 229.6 408.3 229.6 399 239z"/></svg>');
504
+ }
505
+
370
506
  @media (max-width: 620px) {
371
507
  .mikuru-audio {
372
508
  grid-template-columns: 1fr;
@@ -17,36 +17,84 @@
17
17
  </template>
18
18
 
19
19
  <script>
20
- import { computed } from "mikuru";
20
+ import { computed, onMounted, onUnmounted, watch } from "mikuru";
21
21
 
22
22
  const {
23
23
  toasts = [],
24
- position = "bottom-right"
24
+ position = "bottom-right",
25
+ duration = 5000
25
26
  } = defineProps({
26
27
  toasts: Array,
27
- position: String
28
+ position: String,
29
+ duration: Number
28
30
  });
29
31
 
30
32
  const emit = defineEmits(["dismiss"]);
33
+ const timers = new Map();
31
34
  const positionClass = computed(() => `position-${position.value}`);
32
35
  const normalizedToasts = computed(() => {
33
36
  const source = Array.isArray(toasts.value) ? toasts.value : [];
34
37
  return source.map((toast, index) => {
35
38
  const id = toast.id ?? index;
36
39
  const tone = toast.tone || "info";
40
+ const toastDuration = typeof toast.duration === "number" ? toast.duration : duration.value;
37
41
  return {
38
42
  id,
39
43
  title: toast.title || "Notification",
40
44
  message: toast.message || "",
41
45
  toneClass: `tone-${tone}`,
42
- dismissLabel: `Dismiss ${toast.title || "notification"}`
46
+ dismissLabel: `Dismiss ${toast.title || "notification"}`,
47
+ duration: toastDuration
43
48
  };
44
49
  });
45
50
  });
46
51
 
52
+ onMounted(() => {
53
+ syncToastTimers();
54
+ });
55
+
56
+ onUnmounted(() => {
57
+ clearToastTimers();
58
+ });
59
+
60
+ watch(toasts, () => {
61
+ syncToastTimers();
62
+ });
63
+
47
64
  function dismissToast(id) {
65
+ clearToastTimer(id);
48
66
  emit("dismiss", id);
49
67
  }
68
+
69
+ function syncToastTimers() {
70
+ const visibleIds = new Set(normalizedToasts.value.map((toast) => toast.id));
71
+ Array.from(timers.keys()).forEach((id) => {
72
+ if (!visibleIds.has(id)) {
73
+ clearToastTimer(id);
74
+ }
75
+ });
76
+
77
+ normalizedToasts.value.forEach((toast) => {
78
+ if (timers.has(toast.id)) return;
79
+ if (!Number.isFinite(toast.duration) || toast.duration <= 0) return;
80
+ const timer = window.setTimeout(() => {
81
+ timers.delete(toast.id);
82
+ emit("dismiss", toast.id);
83
+ }, toast.duration);
84
+ timers.set(toast.id, timer);
85
+ });
86
+ }
87
+
88
+ function clearToastTimer(id) {
89
+ const timer = timers.get(id);
90
+ if (!timer) return;
91
+ window.clearTimeout(timer);
92
+ timers.delete(id);
93
+ }
94
+
95
+ function clearToastTimers() {
96
+ Array.from(timers.keys()).forEach((id) => clearToastTimer(id));
97
+ }
50
98
  </script>
51
99
 
52
100
  <style scoped>
@@ -31,15 +31,16 @@
31
31
 
32
32
  <div class="top-bar">
33
33
  <strong>{{ title }}</strong>
34
- <span>{{ subtitle }}</span>
34
+ <span :class="{ 'live-badge': isLive }">{{ statusText }}</span>
35
35
  </div>
36
36
 
37
- <button class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
37
+ <button m-if="showPlayControl" class="center-toggle" type="button" @click="togglePlayback" :aria-label="playLabel">
38
38
  <span class="fa-icon large" :class="playIconClass" aria-hidden="true"></span>
39
39
  </button>
40
40
 
41
- <div class="control-shelf">
41
+ <div m-if="showControlShelf" class="control-shelf">
42
42
  <div
43
+ m-if="showSeekControl"
43
44
  class="seek"
44
45
  role="slider"
45
46
  tabindex="0"
@@ -62,25 +63,22 @@
62
63
 
63
64
  <div class="control-row">
64
65
  <div class="left-controls">
65
- <button class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
66
+ <button m-if="showPlayControl" class="icon-button" type="button" @click="togglePlayback" :aria-label="playLabel">
66
67
  <span class="fa-icon" :class="playIconClass" aria-hidden="true"></span>
67
68
  </button>
68
- <button class="icon-button" type="button" @click="stopPlayback" aria-label="Stop video">
69
- <span class="fa-icon icon-stop" aria-hidden="true"></span>
70
- </button>
71
- <button class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
69
+ <button m-if="showMuteControl" class="icon-button" type="button" @click="toggleMute" :aria-label="muteLabel">
72
70
  <span class="fa-icon icon-volume" aria-hidden="true"></span>
73
71
  </button>
74
- <label class="volume">
72
+ <label m-if="showVolumeControl" class="volume">
75
73
  <span>Volume</span>
76
74
  <input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
77
75
  </label>
78
- <span class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
76
+ <span m-if="showTimeControl" class="clock">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
79
77
  </div>
80
78
 
81
79
  <div class="right-controls">
82
- <button class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
83
- <button class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
80
+ <button m-if="showRateControl" class="pill-button" type="button" @click="cycleRate" aria-label="Change playback speed">1x</button>
81
+ <button m-if="showFullscreenControl" class="icon-button fullscreen-button" type="button" @click="toggleFullscreen" aria-label="Fullscreen">
84
82
  <span class="fa-icon" :class="fullscreenIconClass" aria-hidden="true"></span>
85
83
  </button>
86
84
  </div>
@@ -98,13 +96,17 @@ const {
98
96
  poster = "",
99
97
  title = "Mikuru Video",
100
98
  subtitle = "Original player component",
101
- preload = "metadata"
99
+ preload = "metadata",
100
+ controls,
101
+ live = false
102
102
  } = defineProps({
103
103
  src: String,
104
104
  poster: String,
105
105
  title: String,
106
106
  subtitle: String,
107
- preload: String
107
+ preload: String,
108
+ controls: Array,
109
+ live: Boolean
108
110
  });
109
111
 
110
112
  const emit = defineEmits([
@@ -130,6 +132,13 @@ const isFullscreen = ref(false);
130
132
  const isSeeking = ref(false);
131
133
  const pointerInside = ref(false);
132
134
  const controlsVisible = ref(true);
135
+ const allControls = ["play", "seek", "time", "mute", "volume", "rate", "fullscreen"];
136
+ const liveHiddenControls = ["seek", "time", "rate"];
137
+ const activeControls = computed(() => {
138
+ const configuredControls = Array.isArray(controls.value) ? controls.value : allControls;
139
+ return new Set(configuredControls);
140
+ });
141
+ const isLive = computed(() => live.value === true);
133
142
  const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
134
143
  const seekProgress = computed(() => {
135
144
  if (safeDuration.value <= 0) return 0;
@@ -141,6 +150,24 @@ const playLabel = computed(() => isPlaying.value ? "Pause video" : "Play video")
141
150
  const muteLabel = computed(() => muted.value ? "Unmute video" : "Mute video");
142
151
  const playIconClass = computed(() => isPlaying.value ? "icon-pause" : "icon-play");
143
152
  const fullscreenIconClass = computed(() => isFullscreen.value ? "icon-minimize" : "icon-maximize");
153
+ const statusText = computed(() => isLive.value ? "LIVE" : subtitle.value);
154
+ const showPlayControl = computed(() => hasControl("play"));
155
+ const showSeekControl = computed(() => hasControl("seek"));
156
+ const showTimeControl = computed(() => hasControl("time"));
157
+ const showMuteControl = computed(() => hasControl("mute"));
158
+ const showVolumeControl = computed(() => hasControl("volume"));
159
+ const showRateControl = computed(() => hasControl("rate"));
160
+ const showFullscreenControl = computed(() => hasControl("fullscreen"));
161
+ const showControlShelf = computed(() => (
162
+ showPlayControl.value ||
163
+ showSeekControl.value ||
164
+ showTimeControl.value ||
165
+ showMuteControl.value ||
166
+ showVolumeControl.value ||
167
+ showRateControl.value ||
168
+ showFullscreenControl.value ||
169
+ isLive.value
170
+ ));
144
171
  const rates = [1, 1.25, 1.5, 2, 0.75];
145
172
  let playbackRate = 1;
146
173
  let rateIndex = 0;
@@ -175,6 +202,11 @@ function shouldIgnoreMediaEvent() {
175
202
  return performance.now() < ignoreMediaEventsUntil;
176
203
  }
177
204
 
205
+ function hasControl(name) {
206
+ if (isLive.value && liveHiddenControls.includes(name)) return false;
207
+ return activeControls.value.has(name);
208
+ }
209
+
178
210
  function syncMedia() {
179
211
  if (isDisposed || shouldIgnoreMediaEvent()) return;
180
212
  const media = getMedia();
@@ -362,18 +394,6 @@ function seekWithKeyboard(event) {
362
394
  seekTo(nextTime);
363
395
  }
364
396
 
365
- function stopPlayback() {
366
- if (isDisposed) return;
367
- window.setTimeout(() => {
368
- if (isDisposed) return;
369
- const media = getMedia();
370
- if (!media) return;
371
- ignoreMediaEventsFor(300);
372
- media.pause();
373
- media.currentTime = 0;
374
- }, 0);
375
- }
376
-
377
397
  function setVolume(event) {
378
398
  if (isDisposed) return;
379
399
  const nextVolume = Number(event.target.value);
@@ -513,6 +533,20 @@ function formatTime(seconds) {
513
533
  font-size: 0.88rem;
514
534
  }
515
535
 
536
+ .live-badge {
537
+ color: #ffffff;
538
+ font-size: 0.78rem;
539
+ font-weight: 800;
540
+ letter-spacing: 0;
541
+ }
542
+
543
+ .live-badge {
544
+ flex: 0 0 auto;
545
+ padding: 4px 8px;
546
+ border-radius: 999px;
547
+ background: #dc2626;
548
+ }
549
+
516
550
  .center-toggle {
517
551
  position: absolute;
518
552
  top: 50%;
@@ -707,8 +741,7 @@ button:hover {
707
741
  transform: translateX(3px);
708
742
  }
709
743
 
710
- .center-toggle .icon-pause,
711
- .center-toggle .icon-stop {
744
+ .center-toggle .icon-pause {
712
745
  transform: translateX(0);
713
746
  }
714
747
 
@@ -720,10 +753,6 @@ button:hover {
720
753
  mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M176 96C149.5 96 128 117.5 128 144L128 496C128 522.5 149.5 544 176 544L240 544C266.5 544 288 522.5 288 496L288 144C288 117.5 266.5 96 240 96L176 96zM400 96C373.5 96 352 117.5 352 144L352 496C352 522.5 373.5 544 400 544L464 544C490.5 544 512 522.5 512 496L512 144C512 117.5 490.5 96 464 96L400 96z"/></svg>');
721
754
  }
722
755
 
723
- .icon-stop {
724
- mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z"/></svg>');
725
- }
726
-
727
756
  .icon-maximize {
728
757
  mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M264 96L120 96C106.7 96 96 106.7 96 120L96 264C96 273.7 101.8 282.5 110.8 286.2C119.8 289.9 130.1 287.8 137 281L177 241L256 320L177 399L137 359C130.1 352.1 119.8 350.1 110.8 353.8C101.8 357.5 96 366.3 96 376L96 520C96 533.3 106.7 544 120 544L264 544C273.7 544 282.5 538.2 286.2 529.2C289.9 520.2 287.9 509.9 281 503L241 463L320 384L399 463L359 503C352.1 509.9 350.1 520.2 353.8 529.2C357.5 538.2 366.3 544 376 544L520 544C533.3 544 544 533.3 544 520L544 376C544 366.3 538.2 357.5 529.2 353.8C520.2 350.1 509.9 352.1 503 359L463 399L384 320L463 241L503 281C509.9 287.9 520.2 289.9 529.2 286.2C538.2 282.5 544 273.7 544 264L544 120C544 106.7 533.3 96 520 96L376 96C366.3 96 357.5 101.8 353.8 110.8C350.1 119.8 352.2 130.1 359 137L399 177L320 256L241 177L281 137C287.9 130.1 289.9 119.8 286.2 110.8C282.5 101.8 273.7 96 264 96z"/></svg>');
729
758
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikuru",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A compile-first JavaScript framework with Vue-like authoring and Svelte-like generated DOM updates.",
@@ -23,11 +23,21 @@ export type MikuruAudioPlayerEvents = {
23
23
  onRatechange?: (payload: MikuruAudioPlayerEventPayload) => void;
24
24
  };
25
25
 
26
+ export type MikuruAudioPlayerControl =
27
+ | "play"
28
+ | "seek"
29
+ | "time"
30
+ | "skip"
31
+ | "mute"
32
+ | "volume";
33
+
26
34
  export type MikuruAudioPlayerProps = {
27
35
  src: string;
28
36
  title?: string;
29
37
  artist?: string;
30
38
  preload?: string;
39
+ controls?: MikuruAudioPlayerControl[];
40
+ live?: boolean;
31
41
  } & MikuruAudioPlayerEvents;
32
42
 
33
43
  declare const component: MikuruComponent<MikuruAudioPlayerProps>;
@@ -5,11 +5,13 @@ export type MikuruToastItem = {
5
5
  title?: string;
6
6
  message?: string;
7
7
  tone?: "info" | "success" | "warning" | "danger" | string;
8
+ duration?: number;
8
9
  };
9
10
 
10
11
  export type MikuruToastProps = {
11
12
  toasts?: MikuruToastItem[];
12
13
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left" | string;
14
+ duration?: number;
13
15
  };
14
16
 
15
17
  declare const component: MikuruComponent<MikuruToastProps>;
@@ -23,12 +23,23 @@ export type MikuruVideoPlayerEvents = {
23
23
  onRatechange?: (payload: MikuruVideoPlayerEventPayload) => void;
24
24
  };
25
25
 
26
+ export type MikuruVideoPlayerControl =
27
+ | "play"
28
+ | "seek"
29
+ | "time"
30
+ | "mute"
31
+ | "volume"
32
+ | "rate"
33
+ | "fullscreen";
34
+
26
35
  export type MikuruVideoPlayerProps = {
27
36
  src: string;
28
37
  poster?: string;
29
38
  title?: string;
30
39
  subtitle?: string;
31
40
  preload?: string;
41
+ controls?: MikuruVideoPlayerControl[];
42
+ live?: boolean;
32
43
  } & MikuruVideoPlayerEvents;
33
44
 
34
45
  declare const component: MikuruComponent<MikuruVideoPlayerProps>;