mikuru 1.0.33 → 1.0.35
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 +15 -0
- package/README.md +25 -1
- package/components/MikuruAccordion.mikuru +156 -0
- package/components/MikuruAudioPlayer.mikuru +5 -4
- package/components/MikuruCarousel.mikuru +174 -20
- package/components/MikuruCheckbox.mikuru +84 -0
- package/components/MikuruCodeBlock.mikuru +245 -7
- package/components/MikuruCombobox.mikuru +226 -0
- package/components/MikuruDropdown.mikuru +15 -4
- package/components/MikuruFooter.mikuru +121 -0
- package/components/MikuruHeader.mikuru +165 -0
- package/components/MikuruImageViewer.mikuru +1 -3
- package/components/MikuruProgress.mikuru +1 -3
- package/components/MikuruSelect.mikuru +106 -0
- package/components/MikuruSideMenu.mikuru +188 -0
- package/components/MikuruTabs.mikuru +163 -0
- package/components/MikuruTextInput.mikuru +83 -0
- package/components/MikuruTextarea.mikuru +86 -0
- package/components/MikuruToast.mikuru +16 -8
- package/components/MikuruVideoPlayer.mikuru +33 -12
- package/dist/cli/create.js +5 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +3 -2
- package/dist/cli/templates.js.map +1 -1
- package/dist/cli.js +1 -1
- package/package.json +81 -1
- package/templates/video-player/_gitignore +3 -0
- package/templates/video-player/index.html +13 -0
- package/templates/video-player/package.json +19 -0
- package/templates/video-player/public/favicon.svg +4 -0
- package/templates/video-player/src/App.mikuru +92 -0
- package/templates/video-player/src/css-env.d.ts +1 -0
- package/templates/video-player/src/main.ts +10 -0
- package/templates/video-player/src/mikuru-env.d.ts +1 -0
- package/templates/video-player/src/style.css +132 -0
- package/templates/video-player/tsconfig.json +11 -0
- package/templates/video-player/vite.config.ts +6 -0
- package/types/components/MikuruAccordion.d.ts +18 -0
- package/types/components/MikuruCarousel.d.ts +2 -0
- package/types/components/MikuruCheckbox.d.ts +13 -0
- package/types/components/MikuruCombobox.d.ts +21 -0
- package/types/components/MikuruFooter.d.ts +19 -0
- package/types/components/MikuruHeader.d.ts +22 -0
- package/types/components/MikuruSelect.d.ts +21 -0
- package/types/components/MikuruSideMenu.d.ts +22 -0
- package/types/components/MikuruTabs.d.ts +18 -0
- package/types/components/MikuruTextInput.d.ts +16 -0
- package/types/components/MikuruTextarea.d.ts +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.35 - 2026-05-17
|
|
4
|
+
|
|
5
|
+
- Added package-exported tabs, accordion, form controls, select, combobox, header, footer, and side menu components with typed exports and dogfood coverage.
|
|
6
|
+
- Added a `video-player` create template that imports `MikuruVideoPlayer` from the package and demonstrates quality options, controls, and media events.
|
|
7
|
+
- Improved `MikuruCarousel` with CSS-mask arrow icons, optional thumbnail navigation, hidden thumbnail scrollbars, centered active thumbnails, and a 20-image dogfood gallery case.
|
|
8
|
+
- Updated `MikuruSideMenu` collapse controls to use icon buttons.
|
|
9
|
+
- Added lightweight VS Code-style syntax highlighting to `MikuruCodeBlock` for Mikuru/markup, JavaScript/TypeScript, JSON, and CSS snippets.
|
|
10
|
+
|
|
11
|
+
## 1.0.34 - 2026-05-16
|
|
12
|
+
|
|
13
|
+
- Stabilized package component internals so repeated mounts and parent rerenders no longer recreate equivalent derived arrays, Sets, or style objects unnecessarily.
|
|
14
|
+
- Hardened `MikuruCarousel`, `MikuruDropdown`, `MikuruToast`, `MikuruCodeBlock`, and `MikuruVideoPlayer` against recursive update loops when parents pass freshly-created array props with unchanged contents.
|
|
15
|
+
- Switched package component style bindings in `MikuruImageViewer`, `MikuruProgress`, and `MikuruVideoPlayer` to stable string styles to avoid avoidable reactive object churn.
|
|
16
|
+
- Updated `MikuruAudioPlayer` timeline class derivation to return stable class strings.
|
|
17
|
+
|
|
3
18
|
## 1.0.33 - 2026-05-16
|
|
4
19
|
|
|
5
20
|
- Added `MikuruVideoPlayer` sizing props for width, height, and aspect ratio.
|
package/README.md
CHANGED
|
@@ -29,12 +29,19 @@ Use the `basic` template when you want a small component composition example:
|
|
|
29
29
|
npx mikuru create my-basic-app -t basic
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
Use the `video-player` template when you want a Vite app that imports the package-provided `MikuruVideoPlayer` component:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx mikuru create my-video-app -t video-player
|
|
36
|
+
```
|
|
37
|
+
|
|
32
38
|
List available templates:
|
|
33
39
|
|
|
34
40
|
```sh
|
|
35
41
|
npx mikuru --list-templates
|
|
36
42
|
starter - minimal Vite app
|
|
37
43
|
basic - component composition example
|
|
44
|
+
video-player - MikuruVideoPlayer media app
|
|
38
45
|
```
|
|
39
46
|
|
|
40
47
|
Run a dry-run to preview the target, template, and files without writing them:
|
|
@@ -197,6 +204,7 @@ The package also provides the `mikuru` binary:
|
|
|
197
204
|
```sh
|
|
198
205
|
npx mikuru create my-app
|
|
199
206
|
npx mikuru create my-basic-app -t basic
|
|
207
|
+
npx mikuru create my-video-app -t video-player
|
|
200
208
|
npx mikuru --list-templates
|
|
201
209
|
```
|
|
202
210
|
|
|
@@ -274,12 +282,18 @@ The package also includes original Mikuru components:
|
|
|
274
282
|
- `MikuruAudioPlayer.mikuru`: audio playback with configurable control visibility, live mode, seeking, skip controls, volume, and mute.
|
|
275
283
|
- `MikuruImageViewer.mikuru`: image zoom, pan, rotate, reset, and fullscreen controls.
|
|
276
284
|
- `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
|
|
285
|
+
- `MikuruCarousel.mikuru`: image carousel with arrows, dots, keyboard navigation, optional autoplay, and optional thumbnail navigation.
|
|
278
286
|
- `MikuruToast.mikuru`: fixed notification stack with timed auto-dismiss, dismiss events, and tone variants.
|
|
279
287
|
- `MikuruDropdown.mikuru`: menu button with outside-click close, Escape handling, and select events.
|
|
280
288
|
- `MikuruToolTip.mikuru`: hover/focus tooltip with configurable placement.
|
|
281
289
|
- `MikuruProgress.mikuru`: determinate and indeterminate progress indicator.
|
|
282
290
|
- `MikuruCodeBlock.mikuru`: code display with language label, line numbers, and copy action.
|
|
291
|
+
- `MikuruTabs.mikuru`: accessible tab list with controlled `m-model`, keyboard navigation, and slot/fallback panels.
|
|
292
|
+
- `MikuruAccordion.mikuru`: single or multiple disclosure panels with controlled `m-model` and slot/fallback content.
|
|
293
|
+
- `MikuruTextInput.mikuru`, `MikuruTextarea.mikuru`, and `MikuruCheckbox.mikuru`: form controls that emit `update:modelValue`.
|
|
294
|
+
- `MikuruSelect.mikuru`: labeled select control with normalized string/object options.
|
|
295
|
+
- `MikuruCombobox.mikuru`: searchable single-select combobox with outside-click and Escape close.
|
|
296
|
+
- `MikuruHeader.mikuru`, `MikuruFooter.mikuru`, and `MikuruSideMenu.mikuru`: app shell layout primitives with normalized navigation items and selection events.
|
|
283
297
|
|
|
284
298
|
They can be imported from the package:
|
|
285
299
|
|
|
@@ -295,6 +309,16 @@ import MikuruDropdown from "mikuru/components/MikuruDropdown";
|
|
|
295
309
|
import MikuruToolTip from "mikuru/components/MikuruToolTip";
|
|
296
310
|
import MikuruProgress from "mikuru/components/MikuruProgress";
|
|
297
311
|
import MikuruCodeBlock from "mikuru/components/MikuruCodeBlock";
|
|
312
|
+
import MikuruTabs from "mikuru/components/MikuruTabs";
|
|
313
|
+
import MikuruAccordion from "mikuru/components/MikuruAccordion";
|
|
314
|
+
import MikuruTextInput from "mikuru/components/MikuruTextInput";
|
|
315
|
+
import MikuruTextarea from "mikuru/components/MikuruTextarea";
|
|
316
|
+
import MikuruCheckbox from "mikuru/components/MikuruCheckbox";
|
|
317
|
+
import MikuruSelect from "mikuru/components/MikuruSelect";
|
|
318
|
+
import MikuruCombobox from "mikuru/components/MikuruCombobox";
|
|
319
|
+
import MikuruHeader from "mikuru/components/MikuruHeader";
|
|
320
|
+
import MikuruFooter from "mikuru/components/MikuruFooter";
|
|
321
|
+
import MikuruSideMenu from "mikuru/components/MikuruSideMenu";
|
|
298
322
|
</script>
|
|
299
323
|
```
|
|
300
324
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="mikuru-accordion">
|
|
3
|
+
<details
|
|
4
|
+
m-for="item in normalizedItems"
|
|
5
|
+
:key="item.value"
|
|
6
|
+
class="accordion-item"
|
|
7
|
+
:class="{ open: isOpen(item.value) }"
|
|
8
|
+
@toggle="handleToggle(item, $event)"
|
|
9
|
+
>
|
|
10
|
+
<summary
|
|
11
|
+
class="accordion-trigger"
|
|
12
|
+
:aria-disabled="item.disabled ? 'true' : 'false'"
|
|
13
|
+
@click="handleSummaryClick(item, $event)"
|
|
14
|
+
>
|
|
15
|
+
<span>{{ item.label }}</span>
|
|
16
|
+
<span aria-hidden="true">{{ isOpen(item.value) ? "-" : "+" }}</span>
|
|
17
|
+
</summary>
|
|
18
|
+
<div class="accordion-panel">
|
|
19
|
+
<slot :item="item">{{ item.panel }}</slot>
|
|
20
|
+
</div>
|
|
21
|
+
</details>
|
|
22
|
+
</section>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import { ref, watch } from "mikuru";
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
items = [],
|
|
30
|
+
modelValue = "",
|
|
31
|
+
multiple = false
|
|
32
|
+
} = defineProps();
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits(["update:modelValue", "change"]);
|
|
35
|
+
const normalizedItems = ref([]);
|
|
36
|
+
const openValues = ref([]);
|
|
37
|
+
let itemsSignature = "";
|
|
38
|
+
|
|
39
|
+
watch(items, syncItems, { immediate: true });
|
|
40
|
+
watch(modelValue, syncOpenValues, { immediate: true });
|
|
41
|
+
|
|
42
|
+
function syncItems() {
|
|
43
|
+
const source = Array.isArray(items.value) ? items.value : [];
|
|
44
|
+
const nextItems = source.map((item, index) => {
|
|
45
|
+
if (typeof item === "string") {
|
|
46
|
+
return { label: item, value: item, panel: "", disabled: false };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
label: item.label || `Item ${index + 1}`,
|
|
50
|
+
value: item.value ?? item.label ?? index,
|
|
51
|
+
panel: item.panel || "",
|
|
52
|
+
disabled: Boolean(item.disabled)
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const nextSignature = nextItems
|
|
56
|
+
.map((item) => `${item.value}\u0000${item.label}\u0000${item.panel}\u0000${item.disabled}`)
|
|
57
|
+
.join("\u0001");
|
|
58
|
+
if (nextSignature === itemsSignature) return;
|
|
59
|
+
itemsSignature = nextSignature;
|
|
60
|
+
normalizedItems.value = nextItems;
|
|
61
|
+
syncOpenValues();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function syncOpenValues() {
|
|
65
|
+
const source = multiple.value
|
|
66
|
+
? (Array.isArray(modelValue.value) ? modelValue.value : [])
|
|
67
|
+
: (modelValue.value === "" || modelValue.value == null ? [] : [modelValue.value]);
|
|
68
|
+
const availableValues = normalizedItems.value.map((item) => item.value);
|
|
69
|
+
openValues.value = source.filter((value) => availableValues.some((itemValue) => Object.is(itemValue, value)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isOpen(value) {
|
|
73
|
+
return openValues.value.some((itemValue) => Object.is(itemValue, value));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleSummaryClick(item, event) {
|
|
77
|
+
if (!item.disabled) return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleToggle(item, event) {
|
|
82
|
+
if (item.disabled) {
|
|
83
|
+
event.target.open = false;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (event.target.open === isOpen(item.value)) return;
|
|
87
|
+
toggleItem(item);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toggleItem(item) {
|
|
91
|
+
let nextValues;
|
|
92
|
+
if (multiple.value) {
|
|
93
|
+
nextValues = isOpen(item.value)
|
|
94
|
+
? openValues.value.filter((value) => !Object.is(value, item.value))
|
|
95
|
+
: [...openValues.value, item.value];
|
|
96
|
+
} else {
|
|
97
|
+
nextValues = isOpen(item.value) ? [] : [item.value];
|
|
98
|
+
}
|
|
99
|
+
openValues.value = nextValues;
|
|
100
|
+
const payload = multiple.value ? nextValues : nextValues[0] ?? "";
|
|
101
|
+
emit("update:modelValue", payload);
|
|
102
|
+
emit("change", payload);
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
.mikuru-accordion {
|
|
108
|
+
display: grid;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
border: 1px solid #e2e8f0;
|
|
111
|
+
border-radius: 8px;
|
|
112
|
+
background: #ffffff;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.accordion-item + .accordion-item {
|
|
116
|
+
border-top: 1px solid #e2e8f0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.accordion-trigger {
|
|
120
|
+
display: flex;
|
|
121
|
+
width: 100%;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
gap: 12px;
|
|
125
|
+
border: 0;
|
|
126
|
+
padding: 12px 14px;
|
|
127
|
+
color: #0f172a;
|
|
128
|
+
background: #ffffff;
|
|
129
|
+
text-align: left;
|
|
130
|
+
font: inherit;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
list-style: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.accordion-trigger::-webkit-details-marker {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.accordion-trigger:hover,
|
|
140
|
+
.accordion-trigger:focus-visible,
|
|
141
|
+
.accordion-item.open .accordion-trigger {
|
|
142
|
+
background: #f8fafc;
|
|
143
|
+
outline: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.accordion-trigger[aria-disabled="true"] {
|
|
147
|
+
color: #94a3b8;
|
|
148
|
+
cursor: not-allowed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.accordion-panel {
|
|
152
|
+
padding: 0 14px 14px;
|
|
153
|
+
color: #475569;
|
|
154
|
+
line-height: 1.5;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -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
|
-
|
|
116
|
-
|
|
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
|
|
7
|
+
m-for="slide in slides"
|
|
8
8
|
:key="slide.id"
|
|
9
9
|
:aria-label="slide.label"
|
|
10
10
|
>
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
<button class="carousel-arrow previous" type="button" @click="previous" aria-label="Previous slide">
|
|
25
|
-
|
|
25
|
+
<span class="carousel-arrow-icon icon-previous" aria-hidden="true"></span>
|
|
26
26
|
</button>
|
|
27
27
|
<button class="carousel-arrow next" type="button" @click="next" aria-label="Next slide">
|
|
28
|
-
|
|
28
|
+
<span class="carousel-arrow-icon icon-next" aria-hidden="true"></span>
|
|
29
29
|
</button>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
@@ -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
|
|
36
|
+
m-for="slide in slides"
|
|
37
37
|
:key="slide.id"
|
|
38
38
|
type="button"
|
|
39
39
|
:class="{ active: slide.index === activeIndex }"
|
|
@@ -42,17 +42,43 @@
|
|
|
42
42
|
></button>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
|
+
|
|
46
|
+
<div m-if="showThumbnails" class="carousel-thumbnail-shell">
|
|
47
|
+
<button class="thumbnail-scroll previous" type="button" @click="scrollThumbnails(-1)" aria-label="Scroll thumbnails left">
|
|
48
|
+
<span class="carousel-arrow-icon icon-previous" aria-hidden="true"></span>
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<div ref="thumbnailTrackEl" class="carousel-thumbnails" role="tablist" aria-label="Carousel thumbnails">
|
|
52
|
+
<button
|
|
53
|
+
m-for="slide in slides"
|
|
54
|
+
:key="slide.id"
|
|
55
|
+
type="button"
|
|
56
|
+
class="carousel-thumbnail"
|
|
57
|
+
:class="{ active: slide.index === activeIndex }"
|
|
58
|
+
:aria-label="slide.label"
|
|
59
|
+
:aria-selected="slide.index === activeIndex"
|
|
60
|
+
@click="goToSlide(slide.index)"
|
|
61
|
+
>
|
|
62
|
+
<img :src="slide.thumbnail" :alt="slide.alt" />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<button class="thumbnail-scroll next" type="button" @click="scrollThumbnails(1)" aria-label="Scroll thumbnails right">
|
|
67
|
+
<span class="carousel-arrow-icon icon-next" aria-hidden="true"></span>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
45
70
|
</section>
|
|
46
71
|
</template>
|
|
47
72
|
|
|
48
73
|
<script>
|
|
49
|
-
import { computed, onMounted, onUnmounted, ref } from "mikuru";
|
|
74
|
+
import { computed, onMounted, onUnmounted, ref, watch } from "mikuru";
|
|
50
75
|
|
|
51
76
|
const {
|
|
52
77
|
images = [],
|
|
53
78
|
title = "Mikuru Carousel",
|
|
54
79
|
autoplay = false,
|
|
55
80
|
interval = 5000,
|
|
81
|
+
thumbnails = false,
|
|
56
82
|
emptyTitle = "No slides",
|
|
57
83
|
emptyMessage = "Add images to show the carousel."
|
|
58
84
|
} = defineProps({
|
|
@@ -60,46 +86,67 @@ const {
|
|
|
60
86
|
title: String,
|
|
61
87
|
autoplay: Boolean,
|
|
62
88
|
interval: Number,
|
|
89
|
+
thumbnails: Boolean,
|
|
63
90
|
emptyTitle: String,
|
|
64
91
|
emptyMessage: String
|
|
65
92
|
});
|
|
66
93
|
|
|
67
94
|
const activeIndex = ref(0);
|
|
95
|
+
const slides = ref([]);
|
|
96
|
+
const thumbnailTrackEl = ref(null);
|
|
97
|
+
let slidesSignature = "";
|
|
68
98
|
let timer = null;
|
|
99
|
+
let mounted = false;
|
|
69
100
|
|
|
70
|
-
const
|
|
101
|
+
const slideCount = computed(() => slides.value.length);
|
|
102
|
+
const isEmpty = computed(() => slideCount.value === 0);
|
|
103
|
+
const showThumbnails = computed(() => thumbnails.value && slideCount.value > 0);
|
|
104
|
+
const trackStyle = computed(() => `transform: translateX(-${activeIndex.value * 100}%)`);
|
|
105
|
+
const positionLabel = computed(() => {
|
|
106
|
+
if (slideCount.value === 0) return "0 / 0";
|
|
107
|
+
return `${activeIndex.value + 1} / ${slideCount.value}`;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
watch(images, syncSlides, { immediate: true });
|
|
111
|
+
|
|
112
|
+
function syncSlides() {
|
|
71
113
|
const source = Array.isArray(images.value) ? images.value : [];
|
|
72
|
-
|
|
114
|
+
const nextSlides = source.map((item, index) => {
|
|
73
115
|
const src = typeof item === "string" ? item : item.src;
|
|
74
116
|
const alt = typeof item === "string" ? "" : item.alt || item.title || `Slide ${index + 1}`;
|
|
75
117
|
const slideTitle = typeof item === "string" ? `Slide ${index + 1}` : item.title || `Slide ${index + 1}`;
|
|
76
118
|
const caption = typeof item === "string" ? "" : item.caption || "";
|
|
119
|
+
const thumbnail = typeof item === "string" ? item : item.thumbnail || item.src;
|
|
77
120
|
return {
|
|
78
121
|
id: `${src}-${index}`,
|
|
79
122
|
index,
|
|
80
123
|
src,
|
|
124
|
+
thumbnail,
|
|
81
125
|
alt,
|
|
82
126
|
title: slideTitle,
|
|
83
127
|
caption,
|
|
84
128
|
label: `${slideTitle}, ${index + 1} of ${source.length}`
|
|
85
129
|
};
|
|
86
130
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
}
|
|
131
|
+
const nextSignature = nextSlides
|
|
132
|
+
.map((slide) => `${slide.id}\u0000${slide.thumbnail}\u0000${slide.alt}\u0000${slide.title}\u0000${slide.caption}`)
|
|
133
|
+
.join("\u0001");
|
|
134
|
+
if (nextSignature === slidesSignature) return;
|
|
135
|
+
slidesSignature = nextSignature;
|
|
136
|
+
slides.value = nextSlides;
|
|
137
|
+
activeIndex.value = clampIndex(activeIndex.value);
|
|
138
|
+
if (mounted) {
|
|
139
|
+
startAutoplay();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
97
142
|
|
|
98
143
|
onMounted(() => {
|
|
144
|
+
mounted = true;
|
|
99
145
|
startAutoplay();
|
|
100
146
|
});
|
|
101
147
|
|
|
102
148
|
onUnmounted(() => {
|
|
149
|
+
mounted = false;
|
|
103
150
|
stopAutoplay();
|
|
104
151
|
});
|
|
105
152
|
|
|
@@ -112,6 +159,7 @@ function clampIndex(index) {
|
|
|
112
159
|
|
|
113
160
|
function goToSlide(index) {
|
|
114
161
|
activeIndex.value = clampIndex(index);
|
|
162
|
+
centerActiveThumbnail();
|
|
115
163
|
restartAutoplay();
|
|
116
164
|
}
|
|
117
165
|
|
|
@@ -123,6 +171,18 @@ function next() {
|
|
|
123
171
|
goToSlide(activeIndex.value + 1);
|
|
124
172
|
}
|
|
125
173
|
|
|
174
|
+
function scrollThumbnails(direction) {
|
|
175
|
+
goToSlide(activeIndex.value + direction);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function centerActiveThumbnail() {
|
|
179
|
+
window.setTimeout(() => {
|
|
180
|
+
const track = thumbnailTrackEl.value;
|
|
181
|
+
const activeThumbnail = track?.querySelector?.(".carousel-thumbnail.active");
|
|
182
|
+
activeThumbnail?.scrollIntoView?.({ behavior: "smooth", block: "nearest", inline: "center" });
|
|
183
|
+
}, 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
126
186
|
function startAutoplay() {
|
|
127
187
|
stopAutoplay();
|
|
128
188
|
if (!autoplay.value || slideCount.value <= 1) return;
|
|
@@ -234,13 +294,29 @@ function handleKeydown(event) {
|
|
|
234
294
|
border-radius: 999px;
|
|
235
295
|
color: #111827;
|
|
236
296
|
background: #ffffff;
|
|
237
|
-
|
|
238
|
-
font-size: 1.8rem;
|
|
239
|
-
line-height: 1;
|
|
297
|
+
padding: 0;
|
|
240
298
|
transform: translateY(-50%);
|
|
241
299
|
cursor: pointer;
|
|
242
300
|
}
|
|
243
301
|
|
|
302
|
+
.carousel-arrow-icon {
|
|
303
|
+
display: block;
|
|
304
|
+
width: 18px;
|
|
305
|
+
height: 18px;
|
|
306
|
+
background: currentColor;
|
|
307
|
+
mask-position: center;
|
|
308
|
+
mask-repeat: no-repeat;
|
|
309
|
+
mask-size: contain;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.icon-previous {
|
|
313
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M169.4 297.4C156.9 309.9 156.9 330.2 169.4 342.7L361.4 534.7C373.9 547.2 394.2 547.2 406.7 534.7C419.2 522.2 419.2 501.9 406.7 489.4L237.3 320L406.6 150.6C419.1 138.1 419.1 117.8 406.6 105.3C394.1 92.8 373.8 92.8 361.3 105.3L169.3 297.3z"/></svg>');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.icon-next {
|
|
317
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M471.1 297.4C483.6 309.9 483.6 330.2 471.1 342.7L279.1 534.7C266.6 547.2 246.3 547.2 233.8 534.7C221.3 522.2 221.3 501.9 233.8 489.4L403.2 320L233.9 150.6C221.4 138.1 221.4 117.8 233.9 105.3C246.4 92.8 266.7 92.8 279.2 105.3L471.2 297.3z"/></svg>');
|
|
318
|
+
}
|
|
319
|
+
|
|
244
320
|
.previous {
|
|
245
321
|
left: 12px;
|
|
246
322
|
}
|
|
@@ -277,4 +353,82 @@ function handleKeydown(event) {
|
|
|
277
353
|
width: 24px;
|
|
278
354
|
background: #111827;
|
|
279
355
|
}
|
|
356
|
+
|
|
357
|
+
.carousel-thumbnail-shell {
|
|
358
|
+
position: relative;
|
|
359
|
+
display: grid;
|
|
360
|
+
align-items: center;
|
|
361
|
+
padding: 0 42px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.carousel-thumbnails {
|
|
365
|
+
display: grid;
|
|
366
|
+
grid-auto-columns: minmax(86px, 1fr);
|
|
367
|
+
grid-auto-flow: column;
|
|
368
|
+
gap: 8px;
|
|
369
|
+
overflow-x: auto;
|
|
370
|
+
scrollbar-width: none;
|
|
371
|
+
padding-bottom: 2px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.carousel-thumbnails::-webkit-scrollbar {
|
|
375
|
+
display: none;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.carousel-thumbnail {
|
|
379
|
+
display: block;
|
|
380
|
+
aspect-ratio: 1;
|
|
381
|
+
min-width: 86px;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
border: 2px solid transparent;
|
|
384
|
+
border-radius: 7px;
|
|
385
|
+
padding: 0;
|
|
386
|
+
background: #e2e8f0;
|
|
387
|
+
cursor: pointer;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.carousel-thumbnail:hover,
|
|
391
|
+
.carousel-thumbnail:focus-visible,
|
|
392
|
+
.carousel-thumbnail.active {
|
|
393
|
+
border-color: #111827;
|
|
394
|
+
outline: none;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.carousel-thumbnail img {
|
|
398
|
+
display: block;
|
|
399
|
+
width: 100%;
|
|
400
|
+
height: 100%;
|
|
401
|
+
object-fit: cover;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.thumbnail-scroll {
|
|
405
|
+
position: absolute;
|
|
406
|
+
top: 50%;
|
|
407
|
+
z-index: 1;
|
|
408
|
+
display: grid;
|
|
409
|
+
width: 32px;
|
|
410
|
+
height: 32px;
|
|
411
|
+
place-items: center;
|
|
412
|
+
border: 1px solid #cbd5e1;
|
|
413
|
+
border-radius: 999px;
|
|
414
|
+
color: #111827;
|
|
415
|
+
background: #ffffff;
|
|
416
|
+
box-shadow: 0 8px 24px rgb(15 23 42 / 18%);
|
|
417
|
+
padding: 0;
|
|
418
|
+
transform: translateY(-50%);
|
|
419
|
+
cursor: pointer;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.thumbnail-scroll.previous {
|
|
423
|
+
left: 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.thumbnail-scroll.next {
|
|
427
|
+
right: 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.thumbnail-scroll .carousel-arrow-icon {
|
|
431
|
+
width: 14px;
|
|
432
|
+
height: 14px;
|
|
433
|
+
}
|
|
280
434
|
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="mikuru-checkbox">
|
|
3
|
+
<input
|
|
4
|
+
type="checkbox"
|
|
5
|
+
:checked="checked"
|
|
6
|
+
:value="value"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
@change="updateChecked($event)"
|
|
9
|
+
/>
|
|
10
|
+
<span>
|
|
11
|
+
<span class="checkbox-label">{{ label }}</span>
|
|
12
|
+
<small m-if="description">{{ description }}</small>
|
|
13
|
+
</span>
|
|
14
|
+
</label>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import { computed } from "mikuru";
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
label = "Checkbox",
|
|
22
|
+
description = "",
|
|
23
|
+
modelValue = false,
|
|
24
|
+
value = "on",
|
|
25
|
+
disabled = false
|
|
26
|
+
} = defineProps();
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits(["update:modelValue", "change"]);
|
|
29
|
+
|
|
30
|
+
const checked = computed(() => {
|
|
31
|
+
if (Array.isArray(modelValue.value)) {
|
|
32
|
+
return modelValue.value.some((item) => Object.is(item, value.value));
|
|
33
|
+
}
|
|
34
|
+
return Boolean(modelValue.value);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function updateChecked(event) {
|
|
38
|
+
let nextValue;
|
|
39
|
+
if (Array.isArray(modelValue.value)) {
|
|
40
|
+
nextValue = event.target.checked
|
|
41
|
+
? [...modelValue.value, value.value]
|
|
42
|
+
: modelValue.value.filter((item) => !Object.is(item, value.value));
|
|
43
|
+
} else {
|
|
44
|
+
nextValue = event.target.checked;
|
|
45
|
+
}
|
|
46
|
+
emit("update:modelValue", nextValue);
|
|
47
|
+
emit("change", nextValue);
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.mikuru-checkbox {
|
|
53
|
+
display: inline-grid;
|
|
54
|
+
grid-template-columns: auto 1fr;
|
|
55
|
+
gap: 9px;
|
|
56
|
+
align-items: start;
|
|
57
|
+
color: #0f172a;
|
|
58
|
+
font: inherit;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mikuru-checkbox input {
|
|
63
|
+
width: 18px;
|
|
64
|
+
height: 18px;
|
|
65
|
+
margin: 2px 0 0;
|
|
66
|
+
accent-color: #2563eb;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.mikuru-checkbox input:disabled {
|
|
70
|
+
cursor: not-allowed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.checkbox-label {
|
|
74
|
+
display: block;
|
|
75
|
+
font-weight: 650;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.mikuru-checkbox small {
|
|
79
|
+
display: block;
|
|
80
|
+
margin-top: 2px;
|
|
81
|
+
color: #64748b;
|
|
82
|
+
line-height: 1.35;
|
|
83
|
+
}
|
|
84
|
+
</style>
|