mikuru 1.0.33 → 1.0.34

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,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.34 - 2026-05-16
4
+
5
+ - Stabilized package component internals so repeated mounts and parent rerenders no longer recreate equivalent derived arrays, Sets, or style objects unnecessarily.
6
+ - Hardened `MikuruCarousel`, `MikuruDropdown`, `MikuruToast`, `MikuruCodeBlock`, and `MikuruVideoPlayer` against recursive update loops when parents pass freshly-created array props with unchanged contents.
7
+ - Switched package component style bindings in `MikuruImageViewer`, `MikuruProgress`, and `MikuruVideoPlayer` to stable string styles to avoid avoidable reactive object churn.
8
+ - Updated `MikuruAudioPlayer` timeline class derivation to return stable class strings.
9
+
3
10
  ## 1.0.33 - 2026-05-16
4
11
 
5
12
  - Added `MikuruVideoPlayer` sizing props for width, height, and aspect ratio.
@@ -111,10 +111,11 @@ const showSkipControl = computed(() => hasControl("skip"));
111
111
  const showMuteControl = computed(() => hasControl("mute"));
112
112
  const showVolumeControl = computed(() => hasControl("volume"));
113
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
- }));
114
+ const timelineClass = computed(() => {
115
+ if (showSeekControl.value && !showTimeControl.value) return "timeline-seek-only";
116
+ if (showTimeControl.value && !showSeekControl.value) return "timeline-time-only";
117
+ return "";
118
+ });
118
119
  const showControls = computed(() => (
119
120
  showPlayControl.value ||
120
121
  showSkipControl.value ||
@@ -4,7 +4,7 @@
4
4
  <div class="carousel-track" :style="trackStyle">
5
5
  <article
6
6
  class="carousel-slide"
7
- m-for="slide in normalizedSlides"
7
+ m-for="slide in slides"
8
8
  :key="slide.id"
9
9
  :aria-label="slide.label"
10
10
  >
@@ -33,7 +33,7 @@
33
33
  <span>{{ positionLabel }}</span>
34
34
  <div class="carousel-dots" role="tablist" aria-label="Carousel slides">
35
35
  <button
36
- m-for="slide in normalizedSlides"
36
+ m-for="slide in slides"
37
37
  :key="slide.id"
38
38
  type="button"
39
39
  :class="{ active: slide.index === activeIndex }"
@@ -46,7 +46,7 @@
46
46
  </template>
47
47
 
48
48
  <script>
49
- import { computed, onMounted, onUnmounted, ref } from "mikuru";
49
+ import { computed, onMounted, onUnmounted, ref, watch } from "mikuru";
50
50
 
51
51
  const {
52
52
  images = [],
@@ -65,11 +65,24 @@ const {
65
65
  });
66
66
 
67
67
  const activeIndex = ref(0);
68
+ const slides = ref([]);
69
+ let slidesSignature = "";
68
70
  let timer = null;
71
+ let mounted = false;
69
72
 
70
- const normalizedSlides = computed(() => {
73
+ const slideCount = computed(() => slides.value.length);
74
+ const isEmpty = computed(() => slideCount.value === 0);
75
+ const trackStyle = computed(() => `transform: translateX(-${activeIndex.value * 100}%)`);
76
+ const positionLabel = computed(() => {
77
+ if (slideCount.value === 0) return "0 / 0";
78
+ return `${activeIndex.value + 1} / ${slideCount.value}`;
79
+ });
80
+
81
+ watch(images, syncSlides, { immediate: true });
82
+
83
+ function syncSlides() {
71
84
  const source = Array.isArray(images.value) ? images.value : [];
72
- return source.map((item, index) => {
85
+ const nextSlides = source.map((item, index) => {
73
86
  const src = typeof item === "string" ? item : item.src;
74
87
  const alt = typeof item === "string" ? "" : item.alt || item.title || `Slide ${index + 1}`;
75
88
  const slideTitle = typeof item === "string" ? `Slide ${index + 1}` : item.title || `Slide ${index + 1}`;
@@ -84,22 +97,25 @@ const normalizedSlides = computed(() => {
84
97
  label: `${slideTitle}, ${index + 1} of ${source.length}`
85
98
  };
86
99
  });
87
- });
88
- const slideCount = computed(() => normalizedSlides.value.length);
89
- const isEmpty = computed(() => slideCount.value === 0);
90
- const trackStyle = computed(() => ({
91
- transform: `translateX(-${activeIndex.value * 100}%)`
92
- }));
93
- const positionLabel = computed(() => {
94
- if (slideCount.value === 0) return "0 / 0";
95
- return `${activeIndex.value + 1} / ${slideCount.value}`;
96
- });
100
+ const nextSignature = nextSlides
101
+ .map((slide) => `${slide.id}\u0000${slide.alt}\u0000${slide.title}\u0000${slide.caption}`)
102
+ .join("\u0001");
103
+ if (nextSignature === slidesSignature) return;
104
+ slidesSignature = nextSignature;
105
+ slides.value = nextSlides;
106
+ activeIndex.value = clampIndex(activeIndex.value);
107
+ if (mounted) {
108
+ startAutoplay();
109
+ }
110
+ }
97
111
 
98
112
  onMounted(() => {
113
+ mounted = true;
99
114
  startAutoplay();
100
115
  });
101
116
 
102
117
  onUnmounted(() => {
118
+ mounted = false;
103
119
  stopAutoplay();
104
120
  });
105
121
 
@@ -10,7 +10,7 @@
10
10
  </template>
11
11
 
12
12
  <script>
13
- import { computed, ref } from "mikuru";
13
+ import { computed, ref, watch } from "mikuru";
14
14
 
15
15
  const {
16
16
  code = "",
@@ -23,12 +23,16 @@ const {
23
23
  });
24
24
 
25
25
  const copied = ref(false);
26
+ const lines = ref([]);
26
27
  const languageLabel = computed(() => language.value || "text");
27
28
  const copyLabel = computed(() => copied.value ? "Copied" : "Copy");
28
- const lines = computed(() => code.value.split("\n").map((text, index) => ({
29
- number: index + 1,
30
- text
31
- })));
29
+
30
+ watch(code, () => {
31
+ lines.value = code.value.split("\n").map((text, index) => ({
32
+ number: index + 1,
33
+ text
34
+ }));
35
+ }, { immediate: true });
32
36
 
33
37
  async function copyCode() {
34
38
  if (!navigator.clipboard) return;
@@ -29,7 +29,7 @@
29
29
  </template>
30
30
 
31
31
  <script>
32
- import { computed, onMounted, onUnmounted, ref } from "mikuru";
32
+ import { onMounted, onUnmounted, ref, watch } from "mikuru";
33
33
 
34
34
  const {
35
35
  label = "Menu",
@@ -42,9 +42,14 @@ const {
42
42
  const emit = defineEmits(["select"]);
43
43
  const rootEl = ref(null);
44
44
  const isOpen = ref(false);
45
- const normalizedItems = computed(() => {
45
+ const normalizedItems = ref([]);
46
+ let itemsSignature = "";
47
+
48
+ watch(items, syncItems, { immediate: true });
49
+
50
+ function syncItems() {
46
51
  const source = Array.isArray(items.value) ? items.value : [];
47
- return source.map((item, index) => {
52
+ const nextItems = source.map((item, index) => {
48
53
  if (typeof item === "string") {
49
54
  return { label: item, value: item, description: "", disabled: false };
50
55
  }
@@ -55,7 +60,13 @@ const normalizedItems = computed(() => {
55
60
  disabled: Boolean(item.disabled)
56
61
  };
57
62
  });
58
- });
63
+ const nextSignature = nextItems
64
+ .map((item) => `${item.value}\u0000${item.label}\u0000${item.description}\u0000${item.disabled}`)
65
+ .join("\u0001");
66
+ if (nextSignature === itemsSignature) return;
67
+ itemsSignature = nextSignature;
68
+ normalizedItems.value = nextItems;
69
+ }
59
70
 
60
71
  onMounted(() => {
61
72
  document.addEventListener("pointerdown", handleDocumentPointer);
@@ -82,9 +82,7 @@ const label = computed(() => caption.value || alt.value);
82
82
  const captionText = computed(() => caption.value || alt.value);
83
83
  const zoomLabel = computed(() => `${Math.round(zoom.value * 100)}%`);
84
84
  const fullscreenLabel = computed(() => isFullscreen.value ? "Exit fullscreen" : "Enter fullscreen");
85
- const imageStyle = computed(() => ({
86
- transform: `translate(${offsetX.value}px, ${offsetY.value}px) scale(${zoom.value}) rotate(${rotation.value}deg)`
87
- }));
85
+ const imageStyle = computed(() => `transform: translate(${offsetX.value}px, ${offsetY.value}px) scale(${zoom.value}) rotate(${rotation.value}deg)`);
88
86
 
89
87
  onMounted(() => {
90
88
  document.addEventListener("fullscreenchange", syncFullscreen);
@@ -36,9 +36,7 @@ const safeMax = computed(() => max.value > 0 ? max.value : 100);
36
36
  const clampedValue = computed(() => Math.min(Math.max(value.value, 0), safeMax.value));
37
37
  const percent = computed(() => Math.round((clampedValue.value / safeMax.value) * 100));
38
38
  const percentLabel = computed(() => `${percent.value}%`);
39
- const fillStyle = computed(() => ({
40
- width: indeterminate.value ? "45%" : `${percent.value}%`
41
- }));
39
+ const fillStyle = computed(() => `width: ${indeterminate.value ? "45%" : `${percent.value}%`}`);
42
40
  </script>
43
41
 
44
42
  <style scoped>
@@ -17,7 +17,7 @@
17
17
  </template>
18
18
 
19
19
  <script>
20
- import { computed, onMounted, onUnmounted, watch } from "mikuru";
20
+ import { computed, onMounted, onUnmounted, ref, watch } from "mikuru";
21
21
 
22
22
  const {
23
23
  toasts = [],
@@ -32,9 +32,14 @@ const {
32
32
  const emit = defineEmits(["dismiss"]);
33
33
  const timers = new Map();
34
34
  const positionClass = computed(() => `position-${position.value}`);
35
- const normalizedToasts = computed(() => {
35
+ const normalizedToasts = ref([]);
36
+ let toastsSignature = "";
37
+
38
+ watch([toasts, duration], syncToasts, { immediate: true });
39
+
40
+ function syncToasts() {
36
41
  const source = Array.isArray(toasts.value) ? toasts.value : [];
37
- return source.map((toast, index) => {
42
+ const nextToasts = source.map((toast, index) => {
38
43
  const id = toast.id ?? index;
39
44
  const tone = toast.tone || "info";
40
45
  const toastDuration = typeof toast.duration === "number" ? toast.duration : duration.value;
@@ -47,7 +52,14 @@ const normalizedToasts = computed(() => {
47
52
  duration: toastDuration
48
53
  };
49
54
  });
50
- });
55
+ const nextSignature = nextToasts
56
+ .map((toast) => `${toast.id}\u0000${toast.title}\u0000${toast.message}\u0000${toast.toneClass}\u0000${toast.duration}`)
57
+ .join("\u0001");
58
+ if (nextSignature === toastsSignature) return;
59
+ toastsSignature = nextSignature;
60
+ normalizedToasts.value = nextToasts;
61
+ syncToastTimers();
62
+ }
51
63
 
52
64
  onMounted(() => {
53
65
  syncToastTimers();
@@ -57,10 +69,6 @@ onUnmounted(() => {
57
69
  clearToastTimers();
58
70
  });
59
71
 
60
- watch(toasts, () => {
61
- syncToastTimers();
62
- });
63
-
64
72
  function dismissToast(id) {
65
73
  clearToastTimer(id);
66
74
  emit("dismiss", id);
@@ -144,7 +144,7 @@
144
144
  </template>
145
145
 
146
146
  <script>
147
- import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from "mikuru";
147
+ import { computed, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from "mikuru";
148
148
 
149
149
  const {
150
150
  src,
@@ -202,19 +202,40 @@ const controlsVisible = ref(true);
202
202
  const settingsEl = ref(null);
203
203
  const allControls = ["play", "seek", "time", "mute", "volume", "settings", "fullscreen"];
204
204
  const liveHiddenControls = ["seek", "time"];
205
- const activeControls = computed(() => {
205
+ const activeControls = ref(new Set(allControls));
206
+ const normalizedQualityOptions = ref([]);
207
+ let controlsSignature = "";
208
+ let qualityOptionsSignature = "";
209
+
210
+ watch(controls, syncControls, { immediate: true });
211
+ watch([qualityOptions, src, poster], syncQualityOptions, { immediate: true });
212
+
213
+ function syncControls() {
206
214
  const configuredControls = Array.isArray(controls.value) ? controls.value : allControls;
207
- return new Set(configuredControls);
208
- });
209
- const normalizedQualityOptions = computed(() => {
215
+ const nextSignature = configuredControls.join("\u0001");
216
+ if (nextSignature === controlsSignature) return;
217
+ controlsSignature = nextSignature;
218
+ activeControls.value = new Set(configuredControls);
219
+ }
220
+
221
+ function syncQualityOptions() {
210
222
  const source = Array.isArray(qualityOptions.value) ? qualityOptions.value : [];
211
223
  const options = source
212
224
  .map((option, index) => normalizeQualityOption(option, index))
213
225
  .filter(Boolean);
214
- return options.length > 0
226
+ const nextOptions = options.length > 0
215
227
  ? options
216
228
  : [{ id: "auto", label: "Auto", src: src.value, poster: poster.value }];
217
- });
229
+ const nextSignature = nextOptions
230
+ .map((option) => `${option.id}\u0000${option.label}\u0000${option.src}\u0000${option.poster}`)
231
+ .join("\u0001");
232
+ if (nextSignature === qualityOptionsSignature) return;
233
+ qualityOptionsSignature = nextSignature;
234
+ normalizedQualityOptions.value = nextOptions;
235
+ if (!nextOptions.some((option) => option.id === selectedQualityId.value)) {
236
+ selectedQualityId.value = nextOptions[0]?.id || "auto";
237
+ }
238
+ }
218
239
  const selectedQuality = computed(() =>
219
240
  normalizedQualityOptions.value.find((option) => option.id === selectedQualityId.value) ?? normalizedQualityOptions.value[0]
220
241
  );
@@ -223,15 +244,15 @@ const selectedVideoSrc = computed(() => selectedQuality.value?.src || src.value)
223
244
  const selectedPoster = computed(() => selectedQuality.value?.poster ?? poster.value);
224
245
  const playerStyle = computed(() => {
225
246
  const normalizedWidth = normalizeCssSize(width.value);
226
- return normalizedWidth ? { width: normalizedWidth } : null;
247
+ return normalizedWidth ? `width: ${normalizedWidth}` : "";
227
248
  });
228
249
  const screenStyle = computed(() => {
229
250
  const normalizedHeight = normalizeCssSize(height.value);
230
251
  const normalizedAspectRatio = normalizeAspectRatio(aspectRatio.value);
231
- return {
232
- height: normalizedHeight || null,
233
- aspectRatio: normalizedAspectRatio || null
234
- };
252
+ const declarations = [];
253
+ if (normalizedHeight) declarations.push(`height: ${normalizedHeight}`);
254
+ if (normalizedAspectRatio) declarations.push(`aspect-ratio: ${normalizedAspectRatio}`);
255
+ return declarations.join("; ");
235
256
  });
236
257
  const isLive = computed(() => live.value === true);
237
258
  const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikuru",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
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.",