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.
Files changed (49) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +25 -1
  3. package/components/MikuruAccordion.mikuru +156 -0
  4. package/components/MikuruAudioPlayer.mikuru +5 -4
  5. package/components/MikuruCarousel.mikuru +174 -20
  6. package/components/MikuruCheckbox.mikuru +84 -0
  7. package/components/MikuruCodeBlock.mikuru +245 -7
  8. package/components/MikuruCombobox.mikuru +226 -0
  9. package/components/MikuruDropdown.mikuru +15 -4
  10. package/components/MikuruFooter.mikuru +121 -0
  11. package/components/MikuruHeader.mikuru +165 -0
  12. package/components/MikuruImageViewer.mikuru +1 -3
  13. package/components/MikuruProgress.mikuru +1 -3
  14. package/components/MikuruSelect.mikuru +106 -0
  15. package/components/MikuruSideMenu.mikuru +188 -0
  16. package/components/MikuruTabs.mikuru +163 -0
  17. package/components/MikuruTextInput.mikuru +83 -0
  18. package/components/MikuruTextarea.mikuru +86 -0
  19. package/components/MikuruToast.mikuru +16 -8
  20. package/components/MikuruVideoPlayer.mikuru +33 -12
  21. package/dist/cli/create.js +5 -1
  22. package/dist/cli/create.js.map +1 -1
  23. package/dist/cli/templates.d.ts +1 -1
  24. package/dist/cli/templates.js +3 -2
  25. package/dist/cli/templates.js.map +1 -1
  26. package/dist/cli.js +1 -1
  27. package/package.json +81 -1
  28. package/templates/video-player/_gitignore +3 -0
  29. package/templates/video-player/index.html +13 -0
  30. package/templates/video-player/package.json +19 -0
  31. package/templates/video-player/public/favicon.svg +4 -0
  32. package/templates/video-player/src/App.mikuru +92 -0
  33. package/templates/video-player/src/css-env.d.ts +1 -0
  34. package/templates/video-player/src/main.ts +10 -0
  35. package/templates/video-player/src/mikuru-env.d.ts +1 -0
  36. package/templates/video-player/src/style.css +132 -0
  37. package/templates/video-player/tsconfig.json +11 -0
  38. package/templates/video-player/vite.config.ts +6 -0
  39. package/types/components/MikuruAccordion.d.ts +18 -0
  40. package/types/components/MikuruCarousel.d.ts +2 -0
  41. package/types/components/MikuruCheckbox.d.ts +13 -0
  42. package/types/components/MikuruCombobox.d.ts +21 -0
  43. package/types/components/MikuruFooter.d.ts +19 -0
  44. package/types/components/MikuruHeader.d.ts +22 -0
  45. package/types/components/MikuruSelect.d.ts +21 -0
  46. package/types/components/MikuruSideMenu.d.ts +22 -0
  47. package/types/components/MikuruTabs.d.ts +18 -0
  48. package/types/components/MikuruTextInput.d.ts +16 -0
  49. 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 autoplay.
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
- "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
  >
@@ -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 normalizedSlides"
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 normalizedSlides = computed(() => {
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
- return source.map((item, index) => {
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
- 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
- });
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
- font: inherit;
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>