mikuru 1.0.27 → 1.0.28
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 +2 -2
- package/README.md +27 -1
- package/components/MikuruAudioPlayer.mikuru +263 -0
- package/components/MikuruCarousel.mikuru +280 -0
- package/components/MikuruCodeBlock.mikuru +96 -0
- package/components/MikuruDropdown.mikuru +153 -0
- package/components/MikuruImageViewer.mikuru +269 -0
- package/components/MikuruModal.mikuru +159 -0
- package/components/MikuruProgress.mikuru +85 -0
- package/components/MikuruToast.mikuru +128 -0
- package/components/MikuruToolTip.mikuru +95 -0
- package/components/MikuruVideoPlayer.mikuru +635 -0
- package/package.json +82 -1
- package/types/components/MikuruAudioPlayer.d.ts +12 -0
- package/types/components/MikuruCarousel.d.ts +21 -0
- package/types/components/MikuruCodeBlock.d.ts +11 -0
- package/types/components/MikuruDropdown.d.ts +17 -0
- package/types/components/MikuruImageViewer.d.ts +14 -0
- package/types/components/MikuruModal.d.ts +14 -0
- package/types/components/MikuruProgress.d.ts +12 -0
- package/types/components/MikuruToast.d.ts +17 -0
- package/types/components/MikuruToolTip.d.ts +11 -0
- package/types/components/MikuruVideoPlayer.d.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.0.
|
|
3
|
+
## 1.0.28 - 2026-05-15
|
|
4
4
|
|
|
5
|
-
- Added
|
|
5
|
+
- Added package-exported Mikuru video player, audio player, image viewer, modal, carousel, toast, dropdown, tooltip, progress, and code block components with custom controls, template refs, lifecycle cleanup, keyboard-accessible seeking/navigation, volume, mute, playback rate, stop, fullscreen, close/select/dismiss events, progress states, and copy actions.
|
|
6
6
|
- Updated the dogfood media player UI with video-overlay controls, Font Awesome-shaped CSS mask icons, auto-hiding playback controls, and a custom seek track that renders cleanly at the final position.
|
|
7
7
|
- Fixed Vite-routed component CSS requests so style virtual module URLs are keyed by compiled style content, preventing stale `<style scoped>` CSS from being reused after SFC style changes.
|
|
8
8
|
- Added compiler coverage for content-keyed Vite CSS requests so repeated transforms of the same `.mikuru` file with changed styles resolve to distinct style module URLs.
|
package/README.md
CHANGED
|
@@ -268,15 +268,41 @@ npm run dev:mikuru-sample
|
|
|
268
268
|
npm run dev:mikuru-vue-like
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
The
|
|
271
|
+
The package also includes original Mikuru components:
|
|
272
272
|
|
|
273
273
|
- `MikuruVideoPlayer.mikuru`: overlay video controls, div-based seeking, volume/mute, playback rate, stop, and fullscreen controls.
|
|
274
274
|
- `MikuruAudioPlayer.mikuru`: audio playback with seeking, skip controls, volume, and mute.
|
|
275
|
+
- `MikuruImageViewer.mikuru`: image zoom, pan, rotate, reset, and fullscreen controls.
|
|
276
|
+
- `MikuruModal.mikuru`: accessible modal shell with backdrop, Escape close, slots, and close events.
|
|
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.
|
|
279
|
+
- `MikuruDropdown.mikuru`: menu button with outside-click close, Escape handling, and select events.
|
|
280
|
+
- `MikuruToolTip.mikuru`: hover/focus tooltip with configurable placement.
|
|
281
|
+
- `MikuruProgress.mikuru`: determinate and indeterminate progress indicator.
|
|
282
|
+
- `MikuruCodeBlock.mikuru`: code display with language label, line numbers, and copy action.
|
|
283
|
+
|
|
284
|
+
They can be imported from the package:
|
|
285
|
+
|
|
286
|
+
```mikuru
|
|
287
|
+
<script>
|
|
288
|
+
import MikuruVideoPlayer from "mikuru/components/MikuruVideoPlayer";
|
|
289
|
+
import MikuruAudioPlayer from "mikuru/components/MikuruAudioPlayer";
|
|
290
|
+
import MikuruImageViewer from "mikuru/components/MikuruImageViewer";
|
|
291
|
+
import MikuruModal from "mikuru/components/MikuruModal";
|
|
292
|
+
import MikuruCarousel from "mikuru/components/MikuruCarousel";
|
|
293
|
+
import MikuruToast from "mikuru/components/MikuruToast";
|
|
294
|
+
import MikuruDropdown from "mikuru/components/MikuruDropdown";
|
|
295
|
+
import MikuruToolTip from "mikuru/components/MikuruToolTip";
|
|
296
|
+
import MikuruProgress from "mikuru/components/MikuruProgress";
|
|
297
|
+
import MikuruCodeBlock from "mikuru/components/MikuruCodeBlock";
|
|
298
|
+
</script>
|
|
299
|
+
```
|
|
275
300
|
|
|
276
301
|
## Documentation
|
|
277
302
|
|
|
278
303
|
- `CHANGELOG.md` lists published package changes.
|
|
279
304
|
- `docs/npm-usage.md` shows a manual Vite setup for package consumers.
|
|
305
|
+
- `docs/mikuru-components.md` shows usage examples for package-exported Mikuru components.
|
|
280
306
|
- `docs/app-architecture.md` describes how to keep larger Mikuru apps split across components, API modules, stores, forms, auth, and tests.
|
|
281
307
|
- `docs/router.md` documents the runtime router.
|
|
282
308
|
- `docs/production-readiness.md` summarizes debugging, parser, package, SSR, and hydration caveats.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="mikuru-audio" :class="{ 'is-playing': isPlaying }" :aria-label="title">
|
|
3
|
+
<audio
|
|
4
|
+
ref="mediaEl"
|
|
5
|
+
:src="src"
|
|
6
|
+
:preload="preload"
|
|
7
|
+
@loadedmetadata="syncMedia"
|
|
8
|
+
@timeupdate="syncMedia"
|
|
9
|
+
@durationchange="syncMedia"
|
|
10
|
+
@play="markPlaying"
|
|
11
|
+
@pause="markPaused"
|
|
12
|
+
@ended="markPaused"
|
|
13
|
+
></audio>
|
|
14
|
+
|
|
15
|
+
<div class="art">
|
|
16
|
+
<span>{{ initials }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="body">
|
|
20
|
+
<div class="identity">
|
|
21
|
+
<strong>{{ title }}</strong>
|
|
22
|
+
<span>{{ artist }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="timeline">
|
|
26
|
+
<span>{{ formatTime(currentTime) }}</span>
|
|
27
|
+
<input type="range" min="0" :max="safeDuration" step="0.1" :value="currentTime" @input="seek" />
|
|
28
|
+
<span>{{ formatTime(duration) }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="controls">
|
|
32
|
+
<button type="button" @click="skipBackward">-10</button>
|
|
33
|
+
<button class="primary" type="button" @click="togglePlayback" :aria-label="playLabel">
|
|
34
|
+
<span m-if="isPlaying">Pause</span>
|
|
35
|
+
<span m-else>Play</span>
|
|
36
|
+
</button>
|
|
37
|
+
<button type="button" @click="skipForward">+10</button>
|
|
38
|
+
<button type="button" @click="toggleMute" :aria-label="muteLabel">
|
|
39
|
+
<span m-if="muted">Muted</span>
|
|
40
|
+
<span m-else>Sound</span>
|
|
41
|
+
</button>
|
|
42
|
+
<label class="volume">
|
|
43
|
+
<span>Volume</span>
|
|
44
|
+
<input type="range" min="0" max="1" step="0.01" :value="volume" @input="setVolume" />
|
|
45
|
+
</label>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
import { computed, onMounted, ref } from "mikuru";
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
src,
|
|
56
|
+
title = "Mikuru Audio",
|
|
57
|
+
artist = "Original player component",
|
|
58
|
+
preload = "metadata"
|
|
59
|
+
} = defineProps({
|
|
60
|
+
src: String,
|
|
61
|
+
title: String,
|
|
62
|
+
artist: String,
|
|
63
|
+
preload: String
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const mediaEl = ref(null);
|
|
67
|
+
const currentTime = ref(0);
|
|
68
|
+
const duration = ref(0);
|
|
69
|
+
const volume = ref(0.75);
|
|
70
|
+
const muted = ref(false);
|
|
71
|
+
const isPlaying = ref(false);
|
|
72
|
+
const safeDuration = computed(() => duration.value > 0 ? duration.value : 0);
|
|
73
|
+
const playLabel = computed(() => isPlaying.value ? "Pause audio" : "Play audio");
|
|
74
|
+
const muteLabel = computed(() => muted.value ? "Unmute audio" : "Mute audio");
|
|
75
|
+
const initials = computed(() => {
|
|
76
|
+
const words = title.value.split(" ").filter(Boolean);
|
|
77
|
+
return words.slice(0, 2).map((word) => word[0]).join("").toUpperCase() || "M";
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
onMounted(() => {
|
|
81
|
+
applyAudioSettings();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function getMedia() {
|
|
85
|
+
return mediaEl.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function syncMedia() {
|
|
89
|
+
const media = getMedia();
|
|
90
|
+
if (!media) return;
|
|
91
|
+
currentTime.value = media.currentTime || 0;
|
|
92
|
+
duration.value = Number.isFinite(media.duration) ? media.duration : 0;
|
|
93
|
+
volume.value = media.volume;
|
|
94
|
+
muted.value = media.muted;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function applyAudioSettings() {
|
|
98
|
+
const media = getMedia();
|
|
99
|
+
if (!media) return;
|
|
100
|
+
media.volume = volume.value;
|
|
101
|
+
media.muted = muted.value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function markPlaying() {
|
|
105
|
+
isPlaying.value = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function markPaused() {
|
|
109
|
+
isPlaying.value = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function togglePlayback() {
|
|
113
|
+
const media = getMedia();
|
|
114
|
+
if (!media) return;
|
|
115
|
+
if (media.paused) {
|
|
116
|
+
await media.play();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
media.pause();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function seek(event) {
|
|
123
|
+
const media = getMedia();
|
|
124
|
+
const nextTime = Number(event.target.value);
|
|
125
|
+
currentTime.value = nextTime;
|
|
126
|
+
if (media) {
|
|
127
|
+
media.currentTime = nextTime;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setVolume(event) {
|
|
132
|
+
const nextVolume = Number(event.target.value);
|
|
133
|
+
volume.value = nextVolume;
|
|
134
|
+
muted.value = nextVolume === 0;
|
|
135
|
+
applyAudioSettings();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toggleMute() {
|
|
139
|
+
muted.value = !muted.value;
|
|
140
|
+
applyAudioSettings();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function skipBackward() {
|
|
144
|
+
skipBy(-10);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function skipForward() {
|
|
148
|
+
skipBy(10);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function skipBy(offset) {
|
|
152
|
+
const media = getMedia();
|
|
153
|
+
if (!media) return;
|
|
154
|
+
const nextTime = Math.min(Math.max(media.currentTime + offset, 0), safeDuration.value || media.currentTime + offset);
|
|
155
|
+
media.currentTime = nextTime;
|
|
156
|
+
currentTime.value = nextTime;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTime(seconds) {
|
|
160
|
+
const safeSeconds = Number.isFinite(seconds) ? Math.floor(seconds) : 0;
|
|
161
|
+
const minutes = Math.floor(safeSeconds / 60);
|
|
162
|
+
const remainder = String(safeSeconds % 60).padStart(2, "0");
|
|
163
|
+
return minutes + ":" + remainder;
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<style scoped>
|
|
168
|
+
.mikuru-audio {
|
|
169
|
+
display: grid;
|
|
170
|
+
grid-template-columns: 92px minmax(0, 1fr);
|
|
171
|
+
gap: 14px;
|
|
172
|
+
align-items: center;
|
|
173
|
+
padding: 14px;
|
|
174
|
+
border: 1px solid #d1d5db;
|
|
175
|
+
border-radius: 8px;
|
|
176
|
+
color: #172554;
|
|
177
|
+
background: #f8fbff;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.art {
|
|
181
|
+
display: grid;
|
|
182
|
+
place-items: center;
|
|
183
|
+
aspect-ratio: 1;
|
|
184
|
+
border-radius: 8px;
|
|
185
|
+
color: #ffffff;
|
|
186
|
+
background: #0f766e;
|
|
187
|
+
font-size: 1.45rem;
|
|
188
|
+
font-weight: 800;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.body,
|
|
192
|
+
.identity {
|
|
193
|
+
display: grid;
|
|
194
|
+
gap: 8px;
|
|
195
|
+
min-width: 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.identity span,
|
|
199
|
+
.timeline,
|
|
200
|
+
.volume span {
|
|
201
|
+
color: #64748b;
|
|
202
|
+
font-size: 0.9rem;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.timeline {
|
|
206
|
+
display: grid;
|
|
207
|
+
grid-template-columns: 42px minmax(0, 1fr) 42px;
|
|
208
|
+
gap: 8px;
|
|
209
|
+
align-items: center;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.timeline span:last-child {
|
|
213
|
+
text-align: right;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.controls {
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 8px;
|
|
220
|
+
flex-wrap: wrap;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.volume {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 8px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
input[type="range"] {
|
|
230
|
+
accent-color: #0f766e;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
button {
|
|
234
|
+
min-height: 34px;
|
|
235
|
+
padding: 0 11px;
|
|
236
|
+
border: 1px solid #cbd5e1;
|
|
237
|
+
border-radius: 8px;
|
|
238
|
+
color: #172554;
|
|
239
|
+
background: #ffffff;
|
|
240
|
+
font: inherit;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
button:hover {
|
|
245
|
+
border-color: #0f766e;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.primary {
|
|
249
|
+
border-color: #0f766e;
|
|
250
|
+
color: #ffffff;
|
|
251
|
+
background: #0f766e;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@media (max-width: 620px) {
|
|
255
|
+
.mikuru-audio {
|
|
256
|
+
grid-template-columns: 1fr;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.art {
|
|
260
|
+
width: 86px;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="mikuru-carousel" :aria-label="title" @keydown="handleKeydown" tabindex="0">
|
|
3
|
+
<div class="carousel-viewport">
|
|
4
|
+
<div class="carousel-track" :style="trackStyle">
|
|
5
|
+
<article
|
|
6
|
+
class="carousel-slide"
|
|
7
|
+
m-for="slide in normalizedSlides"
|
|
8
|
+
:key="slide.id"
|
|
9
|
+
:aria-label="slide.label"
|
|
10
|
+
>
|
|
11
|
+
<img :src="slide.src" :alt="slide.alt" />
|
|
12
|
+
<div class="slide-caption">
|
|
13
|
+
<strong>{{ slide.title }}</strong>
|
|
14
|
+
<span>{{ slide.caption }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
</article>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div m-if="isEmpty" class="carousel-empty">
|
|
20
|
+
<strong>{{ emptyTitle }}</strong>
|
|
21
|
+
<span>{{ emptyMessage }}</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<button class="carousel-arrow previous" type="button" @click="previous" aria-label="Previous slide">
|
|
25
|
+
‹
|
|
26
|
+
</button>
|
|
27
|
+
<button class="carousel-arrow next" type="button" @click="next" aria-label="Next slide">
|
|
28
|
+
›
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="carousel-footer">
|
|
33
|
+
<span>{{ positionLabel }}</span>
|
|
34
|
+
<div class="carousel-dots" role="tablist" aria-label="Carousel slides">
|
|
35
|
+
<button
|
|
36
|
+
m-for="slide in normalizedSlides"
|
|
37
|
+
:key="slide.id"
|
|
38
|
+
type="button"
|
|
39
|
+
:class="{ active: slide.index === activeIndex }"
|
|
40
|
+
:aria-label="slide.label"
|
|
41
|
+
@click="goToSlide(slide.index)"
|
|
42
|
+
></button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
import { computed, onMounted, onUnmounted, ref } from "mikuru";
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
images = [],
|
|
53
|
+
title = "Mikuru Carousel",
|
|
54
|
+
autoplay = false,
|
|
55
|
+
interval = 5000,
|
|
56
|
+
emptyTitle = "No slides",
|
|
57
|
+
emptyMessage = "Add images to show the carousel."
|
|
58
|
+
} = defineProps({
|
|
59
|
+
images: Array,
|
|
60
|
+
title: String,
|
|
61
|
+
autoplay: Boolean,
|
|
62
|
+
interval: Number,
|
|
63
|
+
emptyTitle: String,
|
|
64
|
+
emptyMessage: String
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const activeIndex = ref(0);
|
|
68
|
+
let timer = null;
|
|
69
|
+
|
|
70
|
+
const normalizedSlides = computed(() => {
|
|
71
|
+
const source = Array.isArray(images.value) ? images.value : [];
|
|
72
|
+
return source.map((item, index) => {
|
|
73
|
+
const src = typeof item === "string" ? item : item.src;
|
|
74
|
+
const alt = typeof item === "string" ? "" : item.alt || item.title || `Slide ${index + 1}`;
|
|
75
|
+
const slideTitle = typeof item === "string" ? `Slide ${index + 1}` : item.title || `Slide ${index + 1}`;
|
|
76
|
+
const caption = typeof item === "string" ? "" : item.caption || "";
|
|
77
|
+
return {
|
|
78
|
+
id: `${src}-${index}`,
|
|
79
|
+
index,
|
|
80
|
+
src,
|
|
81
|
+
alt,
|
|
82
|
+
title: slideTitle,
|
|
83
|
+
caption,
|
|
84
|
+
label: `${slideTitle}, ${index + 1} of ${source.length}`
|
|
85
|
+
};
|
|
86
|
+
});
|
|
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
|
+
});
|
|
97
|
+
|
|
98
|
+
onMounted(() => {
|
|
99
|
+
startAutoplay();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
onUnmounted(() => {
|
|
103
|
+
stopAutoplay();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function clampIndex(index) {
|
|
107
|
+
if (slideCount.value === 0) return 0;
|
|
108
|
+
if (index < 0) return slideCount.value - 1;
|
|
109
|
+
if (index >= slideCount.value) return 0;
|
|
110
|
+
return index;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function goToSlide(index) {
|
|
114
|
+
activeIndex.value = clampIndex(index);
|
|
115
|
+
restartAutoplay();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function previous() {
|
|
119
|
+
goToSlide(activeIndex.value - 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function next() {
|
|
123
|
+
goToSlide(activeIndex.value + 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function startAutoplay() {
|
|
127
|
+
stopAutoplay();
|
|
128
|
+
if (!autoplay.value || slideCount.value <= 1) return;
|
|
129
|
+
timer = window.setInterval(() => {
|
|
130
|
+
activeIndex.value = clampIndex(activeIndex.value + 1);
|
|
131
|
+
}, Math.max(interval.value, 1000));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stopAutoplay() {
|
|
135
|
+
if (timer) {
|
|
136
|
+
window.clearInterval(timer);
|
|
137
|
+
timer = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function restartAutoplay() {
|
|
142
|
+
startAutoplay();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleKeydown(event) {
|
|
146
|
+
if (event.key === "ArrowLeft") {
|
|
147
|
+
previous();
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
} else if (event.key === "ArrowRight") {
|
|
150
|
+
next();
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
} else if (event.key === "Home") {
|
|
153
|
+
goToSlide(0);
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
} else if (event.key === "End") {
|
|
156
|
+
goToSlide(slideCount.value - 1);
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
</script>
|
|
161
|
+
|
|
162
|
+
<style scoped>
|
|
163
|
+
.mikuru-carousel {
|
|
164
|
+
display: grid;
|
|
165
|
+
gap: 10px;
|
|
166
|
+
color: #111827;
|
|
167
|
+
outline: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.mikuru-carousel:focus-visible {
|
|
171
|
+
box-shadow: 0 0 0 3px #38bdf8;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.carousel-viewport {
|
|
175
|
+
position: relative;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
border-radius: 8px;
|
|
178
|
+
background: #111827;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.carousel-track {
|
|
182
|
+
display: flex;
|
|
183
|
+
transition: transform 260ms ease;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.carousel-slide {
|
|
187
|
+
position: relative;
|
|
188
|
+
flex: 0 0 100%;
|
|
189
|
+
min-height: 300px;
|
|
190
|
+
margin: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.carousel-slide img {
|
|
194
|
+
display: block;
|
|
195
|
+
width: 100%;
|
|
196
|
+
height: 100%;
|
|
197
|
+
min-height: 300px;
|
|
198
|
+
object-fit: cover;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.slide-caption {
|
|
202
|
+
position: absolute;
|
|
203
|
+
right: 0;
|
|
204
|
+
bottom: 0;
|
|
205
|
+
left: 0;
|
|
206
|
+
display: grid;
|
|
207
|
+
gap: 3px;
|
|
208
|
+
padding: 18px;
|
|
209
|
+
color: #f8fafc;
|
|
210
|
+
background: rgb(15 23 42 / 72%);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.slide-caption strong,
|
|
214
|
+
.slide-caption span {
|
|
215
|
+
overflow-wrap: anywhere;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.carousel-empty {
|
|
219
|
+
display: grid;
|
|
220
|
+
min-height: 240px;
|
|
221
|
+
place-items: center;
|
|
222
|
+
gap: 4px;
|
|
223
|
+
color: #cbd5e1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.carousel-arrow {
|
|
227
|
+
position: absolute;
|
|
228
|
+
top: 50%;
|
|
229
|
+
display: grid;
|
|
230
|
+
width: 40px;
|
|
231
|
+
height: 40px;
|
|
232
|
+
place-items: center;
|
|
233
|
+
border: 0;
|
|
234
|
+
border-radius: 999px;
|
|
235
|
+
color: #111827;
|
|
236
|
+
background: #ffffff;
|
|
237
|
+
font: inherit;
|
|
238
|
+
font-size: 1.8rem;
|
|
239
|
+
line-height: 1;
|
|
240
|
+
transform: translateY(-50%);
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.previous {
|
|
245
|
+
left: 12px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.next {
|
|
249
|
+
right: 12px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.carousel-footer {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
justify-content: space-between;
|
|
256
|
+
gap: 12px;
|
|
257
|
+
color: #475569;
|
|
258
|
+
font-size: 0.9rem;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.carousel-dots {
|
|
262
|
+
display: flex;
|
|
263
|
+
gap: 6px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.carousel-dots button {
|
|
267
|
+
width: 9px;
|
|
268
|
+
height: 9px;
|
|
269
|
+
border: 0;
|
|
270
|
+
border-radius: 999px;
|
|
271
|
+
padding: 0;
|
|
272
|
+
background: #94a3b8;
|
|
273
|
+
cursor: pointer;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.carousel-dots button.active {
|
|
277
|
+
width: 24px;
|
|
278
|
+
background: #111827;
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<figure class="mikuru-code-block">
|
|
3
|
+
<figcaption class="code-header">
|
|
4
|
+
<span>{{ languageLabel }}</span>
|
|
5
|
+
<button type="button" @click="copyCode">{{ copyLabel }}</button>
|
|
6
|
+
</figcaption>
|
|
7
|
+
<pre><code><span m-for="line in lines" :key="line.number" class="code-line"><span m-if="showLineNumbers" class="line-number">{{ line.number }}</span><span>{{ line.text }}</span>
|
|
8
|
+
</span></code></pre>
|
|
9
|
+
</figure>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
import { computed, ref } from "mikuru";
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
code = "",
|
|
17
|
+
language = "text",
|
|
18
|
+
showLineNumbers = true
|
|
19
|
+
} = defineProps({
|
|
20
|
+
code: String,
|
|
21
|
+
language: String,
|
|
22
|
+
showLineNumbers: Boolean
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const copied = ref(false);
|
|
26
|
+
const languageLabel = computed(() => language.value || "text");
|
|
27
|
+
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
|
+
})));
|
|
32
|
+
|
|
33
|
+
async function copyCode() {
|
|
34
|
+
if (!navigator.clipboard) return;
|
|
35
|
+
await navigator.clipboard.writeText(code.value);
|
|
36
|
+
copied.value = true;
|
|
37
|
+
window.setTimeout(() => {
|
|
38
|
+
copied.value = false;
|
|
39
|
+
}, 1400);
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.mikuru-code-block {
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
margin: 0;
|
|
47
|
+
border: 1px solid #1e293b;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
background: #0f172a;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.code-header {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
gap: 12px;
|
|
57
|
+
padding: 9px 12px;
|
|
58
|
+
color: #cbd5e1;
|
|
59
|
+
background: #111827;
|
|
60
|
+
font-size: 0.85rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.code-header button {
|
|
64
|
+
border: 1px solid #334155;
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
padding: 6px 9px;
|
|
67
|
+
color: #f8fafc;
|
|
68
|
+
background: #1f2937;
|
|
69
|
+
font: inherit;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pre {
|
|
74
|
+
margin: 0;
|
|
75
|
+
overflow: auto;
|
|
76
|
+
padding: 12px 0;
|
|
77
|
+
color: #e5e7eb;
|
|
78
|
+
font: 0.9rem/1.55 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.code-line {
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-columns: auto 1fr;
|
|
84
|
+
min-width: max-content;
|
|
85
|
+
padding: 0 14px;
|
|
86
|
+
white-space: pre;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.line-number {
|
|
90
|
+
min-width: 3ch;
|
|
91
|
+
margin-right: 14px;
|
|
92
|
+
color: #64748b;
|
|
93
|
+
text-align: right;
|
|
94
|
+
user-select: none;
|
|
95
|
+
}
|
|
96
|
+
</style>
|