mikuru 1.0.39 → 1.0.40

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
+ ## Unreleased
4
+
5
+ ## 1.0.40 - 2026-05-18
6
+
7
+ - Added package-exported `MikuruForm`, `MikuruField`, `MikuruFormMessage`, `MikuruPasswordInput`, `MikuruNumberInput`, `MikuruInputOtp`, `MikuruComboboxMulti`, `MikuruFilterBar`, `MikuruDataToolbar`, and `MikuruVirtualList` components with typed exports and dogfood coverage.
8
+ - Added package-exported `MikuruEmbedPlayer` for iframe-based hosted video embeds across YouTube, Vimeo, Dailymotion, Twitch, Niconico, TikTok, Bilibili, Wistia, and generic embed URLs.
9
+
3
10
  ## 1.0.39 - 2026-05-18
4
11
 
5
12
  - Added package-exported `MikuruPopover`, `MikuruAlertDialog`, `MikuruTable`, `MikuruPagination`, `MikuruDatePicker`, `MikuruMarkdownEditor`, and `MikuruWysiwygEditor` components with typed exports and dogfood coverage.
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <section class="mikuru-combobox-multi" ref="rootEl">
3
+ <label :for="inputId">{{ label }}</label>
4
+ <div class="combo-box">
5
+ <MikuruChip
6
+ m-for="item in selectedItems"
7
+ :key="item.value"
8
+ :label="item.label"
9
+ tone="info"
10
+ removable
11
+ @remove="removeItem(item)"
12
+ />
13
+ <input
14
+ :id="inputId"
15
+ :value="query"
16
+ :placeholder="placeholder"
17
+ @focus="openList"
18
+ @input="updateQuery($event)"
19
+ />
20
+ </div>
21
+ <ul m-if="isOpen && filteredOptions.length" role="listbox">
22
+ <li m-for="option in filteredOptions" :key="option.value">
23
+ <button type="button" @click="selectOption(option)">{{ option.label }}</button>
24
+ </li>
25
+ </ul>
26
+ </section>
27
+ </template>
28
+
29
+ <script>
30
+ import { computed, onMounted, onUnmounted, ref, watch } from "mikuru";
31
+ import MikuruChip from "./MikuruChip.mikuru";
32
+
33
+ const {
34
+ label = "Multi select",
35
+ modelValue = [],
36
+ options = [],
37
+ placeholder = "Search options..."
38
+ } = defineProps({
39
+ label: String,
40
+ modelValue: Array,
41
+ options: Array,
42
+ placeholder: String
43
+ });
44
+
45
+ const emit = defineEmits(["update:modelValue", "change"]);
46
+ const inputId = `mikuru-combobox-multi-${Math.random().toString(36).slice(2)}`;
47
+ const rootEl = ref(null);
48
+ const isOpen = ref(false);
49
+ const query = ref("");
50
+ const values = ref([]);
51
+
52
+ watch(modelValue, () => {
53
+ values.value = Array.from(modelValue.value || []);
54
+ }, { immediate: true });
55
+
56
+ onMounted(() => document.addEventListener("pointerdown", handlePointerDown));
57
+ onUnmounted(() => document.removeEventListener("pointerdown", handlePointerDown));
58
+
59
+ const selectedItems = computed(() => options.value.filter((option) => values.value.includes(option.value)));
60
+ const filteredOptions = computed(() => {
61
+ const term = query.value.trim().toLowerCase();
62
+ return options.value.filter((option) => {
63
+ return !values.value.includes(option.value) && (!term || option.label.toLowerCase().includes(term));
64
+ });
65
+ });
66
+
67
+ function openList() {
68
+ isOpen.value = true;
69
+ }
70
+
71
+ function updateQuery(event) {
72
+ query.value = event.target.value;
73
+ openList();
74
+ }
75
+
76
+ function selectOption(option) {
77
+ updateValues([...values.value, option.value]);
78
+ query.value = "";
79
+ }
80
+
81
+ function removeItem(item) {
82
+ updateValues(values.value.filter((value) => value !== item.value));
83
+ }
84
+
85
+ function updateValues(nextValues) {
86
+ values.value = nextValues;
87
+ emit("update:modelValue", nextValues);
88
+ emit("change", nextValues);
89
+ }
90
+
91
+ function handlePointerDown(event) {
92
+ if (!rootEl.value?.contains(event.target)) isOpen.value = false;
93
+ }
94
+ </script>
95
+
96
+ <style scoped>
97
+ .mikuru-combobox-multi {
98
+ position: relative;
99
+ display: grid;
100
+ gap: 6px;
101
+ color: #111827;
102
+ font: inherit;
103
+ }
104
+
105
+ label {
106
+ font-weight: 650;
107
+ }
108
+
109
+ .combo-box {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ align-items: center;
113
+ gap: 6px;
114
+ border: 1px solid #cbd5e1;
115
+ border-radius: 8px;
116
+ padding: 7px;
117
+ background: #ffffff;
118
+ }
119
+
120
+ input {
121
+ min-width: 120px;
122
+ flex: 1 1 120px;
123
+ border: 0;
124
+ padding: 5px;
125
+ outline: none;
126
+ font: inherit;
127
+ }
128
+
129
+ ul {
130
+ position: absolute;
131
+ z-index: 20;
132
+ top: calc(100% + 4px);
133
+ left: 0;
134
+ right: 0;
135
+ display: grid;
136
+ gap: 2px;
137
+ margin: 0;
138
+ border: 1px solid #e5e7eb;
139
+ border-radius: 8px;
140
+ padding: 6px;
141
+ background: #ffffff;
142
+ box-shadow: 0 18px 48px rgb(15 23 42 / 16%);
143
+ list-style: none;
144
+ }
145
+
146
+ button {
147
+ width: 100%;
148
+ border: 0;
149
+ border-radius: 6px;
150
+ padding: 8px;
151
+ background: transparent;
152
+ font: inherit;
153
+ text-align: left;
154
+ cursor: pointer;
155
+ }
156
+
157
+ button:hover {
158
+ background: #eff6ff;
159
+ }
160
+ </style>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <header class="mikuru-data-toolbar">
3
+ <div>
4
+ <strong>{{ title }}</strong>
5
+ <small m-if="description">{{ description }}</small>
6
+ </div>
7
+ <div class="toolbar-actions">
8
+ <button
9
+ m-for="action in actions"
10
+ :key="action.id || action.label"
11
+ type="button"
12
+ :disabled="action.disabled"
13
+ @click="selectAction(action)"
14
+ >{{ action.label }}</button>
15
+ <slot></slot>
16
+ </div>
17
+ </header>
18
+ </template>
19
+
20
+ <script>
21
+ const {
22
+ title = "Data",
23
+ description = "",
24
+ actions = []
25
+ } = defineProps({
26
+ title: String,
27
+ description: String,
28
+ actions: Array
29
+ });
30
+
31
+ const emit = defineEmits(["action"]);
32
+
33
+ function selectAction(action) {
34
+ if (action.disabled) return;
35
+ emit("action", action);
36
+ }
37
+ </script>
38
+
39
+ <style scoped>
40
+ .mikuru-data-toolbar {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ gap: 12px;
45
+ border: 1px solid #e5e7eb;
46
+ border-radius: 8px;
47
+ padding: 12px;
48
+ background: #ffffff;
49
+ }
50
+
51
+ .mikuru-data-toolbar > div:first-child {
52
+ display: grid;
53
+ gap: 2px;
54
+ }
55
+
56
+ small {
57
+ color: #64748b;
58
+ }
59
+
60
+ .toolbar-actions {
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ gap: 6px;
64
+ }
65
+
66
+ button {
67
+ border: 1px solid #cbd5e1;
68
+ border-radius: 8px;
69
+ padding: 8px 10px;
70
+ color: #111827;
71
+ background: #ffffff;
72
+ font: inherit;
73
+ cursor: pointer;
74
+ }
75
+ </style>
@@ -0,0 +1,429 @@
1
+ <template>
2
+ <figure class="mikuru-embed-player" :style="playerStyle" :data-provider="resolvedProvider">
3
+ <div class="embed-frame" :style="frameStyle">
4
+ <iframe
5
+ m-if="embedSrc"
6
+ class="embed-iframe"
7
+ :src="embedSrc"
8
+ :title="resolvedTitle"
9
+ :loading="loading"
10
+ :allow="allowText"
11
+ :referrerpolicy="referrerPolicy"
12
+ :sandbox="sandboxValue"
13
+ allowfullscreen
14
+ @load="handleLoad"
15
+ ></iframe>
16
+ <div m-else class="embed-empty" role="status">
17
+ <strong>{{ emptyTitle }}</strong>
18
+ <span>{{ emptyMessage }}</span>
19
+ </div>
20
+ </div>
21
+
22
+ <figcaption m-if="showCaption" class="embed-caption">
23
+ <strong>{{ resolvedTitle }}</strong>
24
+ <span>{{ captionText }}</span>
25
+ </figcaption>
26
+ </figure>
27
+ </template>
28
+
29
+ <script>
30
+ import { computed } from "mikuru";
31
+
32
+ const {
33
+ url = "",
34
+ provider = "auto",
35
+ videoId = "",
36
+ title = "Embedded video",
37
+ caption = "",
38
+ width = "100%",
39
+ height = "",
40
+ aspectRatio = "16 / 9",
41
+ autoplay = false,
42
+ muted = false,
43
+ controls = true,
44
+ loop = false,
45
+ privacy = false,
46
+ start = 0,
47
+ end = 0,
48
+ playlist = "",
49
+ parent = "",
50
+ loading = "lazy",
51
+ allow = "",
52
+ referrerPolicy = "strict-origin-when-cross-origin",
53
+ sandbox = "",
54
+ emptyTitle = "Unsupported video URL",
55
+ emptyMessage = "Provide a supported video URL, or pass provider and videoId explicitly."
56
+ } = defineProps();
57
+
58
+ const emit = defineEmits(["load", "unsupported"]);
59
+
60
+ const detected = computed(() => resolveVideo(url.value, provider.value, videoId.value));
61
+ const resolvedProvider = computed(() => detected.value.provider || normalizeProvider(provider.value));
62
+ const resolvedTitle = computed(() => title.value || providerLabel(resolvedProvider.value));
63
+ const captionText = computed(() => caption.value || providerLabel(resolvedProvider.value));
64
+ const showCaption = computed(() => Boolean(title.value || caption.value));
65
+ const sandboxValue = computed(() => sandbox.value || null);
66
+
67
+ const playerStyle = computed(() => {
68
+ return [
69
+ width.value ? `width: ${formatSize(width.value)}` : "",
70
+ height.value ? `height: ${formatSize(height.value)}` : ""
71
+ ].filter(Boolean).join("; ");
72
+ });
73
+
74
+ const frameStyle = computed(() => {
75
+ return [
76
+ `aspect-ratio: ${aspectRatio.value}`,
77
+ height.value ? "height: 100%" : ""
78
+ ].filter(Boolean).join("; ");
79
+ });
80
+
81
+ const allowText = computed(() => {
82
+ return allow.value || "accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share";
83
+ });
84
+
85
+ const embedSrc = computed(() => {
86
+ const info = detected.value;
87
+ if (!info.provider || !info.id) {
88
+ if (info.src) return info.src;
89
+ emit("unsupported", { url: url.value, provider: provider.value, videoId: videoId.value });
90
+ return "";
91
+ }
92
+ return buildEmbedSrc(info.provider, info.id, info);
93
+ });
94
+
95
+ function handleLoad(event) {
96
+ emit("load", {
97
+ provider: resolvedProvider.value,
98
+ videoId: detected.value.id || "",
99
+ src: embedSrc.value,
100
+ nativeEvent: event
101
+ });
102
+ }
103
+
104
+ function resolveVideo(sourceUrl, sourceProvider, sourceVideoId) {
105
+ const explicitProvider = normalizeProvider(sourceProvider);
106
+ if (sourceVideoId) {
107
+ return { provider: explicitProvider === "auto" ? "" : explicitProvider, id: sourceVideoId };
108
+ }
109
+
110
+ const parsed = parseUrl(sourceUrl);
111
+ if (!parsed) {
112
+ return explicitProvider && explicitProvider !== "auto"
113
+ ? { provider: explicitProvider, id: sourceUrl }
114
+ : { provider: "", id: "", src: "" };
115
+ }
116
+
117
+ if (explicitProvider && explicitProvider !== "auto" && explicitProvider !== "generic") {
118
+ return { provider: explicitProvider, id: idFromProvider(explicitProvider, parsed) || sourceUrl };
119
+ }
120
+
121
+ if (explicitProvider === "generic") return { provider: "generic", id: "", src: sourceUrl };
122
+
123
+ const host = parsed.hostname.replace(/^www\./, "");
124
+ if (host === "youtu.be" || host.endsWith("youtube.com") || host.endsWith("youtube-nocookie.com")) {
125
+ return { provider: "youtube", id: youtubeId(parsed) };
126
+ }
127
+ if (host === "vimeo.com" || host === "player.vimeo.com") {
128
+ return { provider: "vimeo", id: vimeoId(parsed) };
129
+ }
130
+ if (host === "dai.ly" || host.endsWith("dailymotion.com")) {
131
+ return { provider: "dailymotion", id: dailymotionId(parsed) };
132
+ }
133
+ if (host.endsWith("twitch.tv")) {
134
+ return twitchInfo(parsed);
135
+ }
136
+ if (host.endsWith("nicovideo.jp") || host.endsWith("nico.ms")) {
137
+ return { provider: "niconico", id: niconicoId(parsed) };
138
+ }
139
+ if (host.endsWith("tiktok.com")) {
140
+ return { provider: "tiktok", id: tiktokId(parsed) };
141
+ }
142
+ if (host.endsWith("bilibili.com") || host === "b23.tv") {
143
+ return { provider: "bilibili", id: bilibiliId(parsed) };
144
+ }
145
+ if (host.endsWith("wistia.com") || host.endsWith("wistia.net")) {
146
+ return { provider: "wistia", id: wistiaId(parsed) };
147
+ }
148
+ if (parsed.pathname.includes("/embed/")) {
149
+ return { provider: "generic", id: "", src: sourceUrl };
150
+ }
151
+ return { provider: "", id: "", src: "" };
152
+ }
153
+
154
+ function buildEmbedSrc(sourceProvider, id, info) {
155
+ if (sourceProvider === "generic") return info.src || url.value;
156
+
157
+ if (sourceProvider === "youtube") {
158
+ const base = privacy.value ? "https://www.youtube-nocookie.com/embed/" : "https://www.youtube.com/embed/";
159
+ const params = createParams({
160
+ autoplay: autoplay.value ? "1" : "",
161
+ mute: muted.value ? "1" : "",
162
+ controls: controls.value ? "" : "0",
163
+ start: start.value > 0 ? start.value : "",
164
+ end: end.value > 0 ? end.value : "",
165
+ loop: loop.value ? "1" : "",
166
+ playlist: loop.value ? playlist.value || id : playlist.value
167
+ });
168
+ return `${base}${encodeURIComponent(id)}${params}`;
169
+ }
170
+
171
+ if (sourceProvider === "vimeo") {
172
+ const params = createParams({
173
+ autoplay: autoplay.value ? "1" : "",
174
+ muted: muted.value ? "1" : "",
175
+ controls: controls.value ? "" : "0",
176
+ loop: loop.value ? "1" : ""
177
+ });
178
+ const hash = start.value > 0 ? `#t=${Math.floor(start.value)}s` : "";
179
+ return `https://player.vimeo.com/video/${encodeURIComponent(id)}${params}${hash}`;
180
+ }
181
+
182
+ if (sourceProvider === "dailymotion") {
183
+ const params = createParams({
184
+ autoplay: autoplay.value ? "1" : "",
185
+ mute: muted.value ? "1" : "",
186
+ controls: controls.value ? "" : "0",
187
+ start: start.value > 0 ? start.value : "",
188
+ "queue-enable": playlist.value ? "1" : ""
189
+ });
190
+ return `https://www.dailymotion.com/embed/video/${encodeURIComponent(id)}${params}`;
191
+ }
192
+
193
+ if (sourceProvider === "twitch") {
194
+ const twitchParent = parent.value || currentHost();
195
+ const params = createParams({
196
+ parent: twitchParent,
197
+ autoplay: autoplay.value ? "true" : "false",
198
+ muted: muted.value ? "true" : "",
199
+ time: start.value > 0 ? `${Math.floor(start.value)}s` : ""
200
+ });
201
+ if (info.kind === "clip") return `https://clips.twitch.tv/embed?clip=${encodeURIComponent(id)}${params ? `&${params.slice(1)}` : ""}`;
202
+ if (info.kind === "video") return `https://player.twitch.tv/?video=${encodeURIComponent(id)}${params ? `&${params.slice(1)}` : ""}`;
203
+ return `https://player.twitch.tv/?channel=${encodeURIComponent(id)}${params ? `&${params.slice(1)}` : ""}`;
204
+ }
205
+
206
+ if (sourceProvider === "niconico") {
207
+ const params = createParams({
208
+ autoplay: autoplay.value ? "1" : "",
209
+ start: start.value > 0 ? start.value : ""
210
+ });
211
+ return `https://embed.nicovideo.jp/watch/${encodeURIComponent(id)}${params}`;
212
+ }
213
+
214
+ if (sourceProvider === "tiktok") {
215
+ return `https://www.tiktok.com/embed/v2/${encodeURIComponent(id)}`;
216
+ }
217
+
218
+ if (sourceProvider === "bilibili") {
219
+ const key = /^av\d+$/i.test(id) ? "aid" : "bvid";
220
+ const params = createParams({
221
+ [key]: id,
222
+ autoplay: autoplay.value ? "1" : "0",
223
+ muted: muted.value ? "1" : "",
224
+ t: start.value > 0 ? start.value : ""
225
+ });
226
+ return `https://player.bilibili.com/player.html${params}`;
227
+ }
228
+
229
+ if (sourceProvider === "wistia") {
230
+ const params = createParams({
231
+ videoFoam: "true",
232
+ autoPlay: autoplay.value ? "true" : "",
233
+ muted: muted.value ? "true" : ""
234
+ });
235
+ return `https://fast.wistia.net/embed/iframe/${encodeURIComponent(id)}${params}`;
236
+ }
237
+
238
+ return "";
239
+ }
240
+
241
+ function normalizeProvider(value) {
242
+ return String(value || "auto").trim().toLowerCase();
243
+ }
244
+
245
+ function parseUrl(value) {
246
+ if (!value) return null;
247
+ try {
248
+ return new URL(value);
249
+ } catch {
250
+ try {
251
+ return new URL(`https://${value}`);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ }
257
+
258
+ function pathSegments(parsed) {
259
+ return parsed.pathname.split("/").filter(Boolean);
260
+ }
261
+
262
+ function idFromProvider(sourceProvider, parsed) {
263
+ const extractors = {
264
+ youtube: youtubeId,
265
+ vimeo: vimeoId,
266
+ dailymotion: dailymotionId,
267
+ niconico: niconicoId,
268
+ tiktok: tiktokId,
269
+ bilibili: bilibiliId,
270
+ wistia: wistiaId
271
+ };
272
+ return extractors[sourceProvider] ? extractors[sourceProvider](parsed) : "";
273
+ }
274
+
275
+ function youtubeId(parsed) {
276
+ const parts = pathSegments(parsed);
277
+ if (parsed.hostname.replace(/^www\./, "") === "youtu.be") return parts[0] || "";
278
+ if (parts[0] === "embed" || parts[0] === "shorts" || parts[0] === "live") return parts[1] || "";
279
+ return parsed.searchParams.get("v") || "";
280
+ }
281
+
282
+ function vimeoId(parsed) {
283
+ const parts = pathSegments(parsed);
284
+ if (parts[0] === "video") return parts[1] || "";
285
+ return parts.find((part) => /^\d+$/.test(part)) || "";
286
+ }
287
+
288
+ function dailymotionId(parsed) {
289
+ const parts = pathSegments(parsed);
290
+ if (parsed.hostname.replace(/^www\./, "") === "dai.ly") return parts[0] || "";
291
+ if (parts[0] === "embed" && parts[1] === "video") return parts[2] || "";
292
+ if (parts[0] === "video") return parts[1] || "";
293
+ return "";
294
+ }
295
+
296
+ function twitchInfo(parsed) {
297
+ const parts = pathSegments(parsed);
298
+ if (parsed.hostname === "clips.twitch.tv") return { provider: "twitch", id: parts[0] || "", kind: "clip" };
299
+ if (parts[0] === "videos") return { provider: "twitch", id: parts[1] || "", kind: "video" };
300
+ if (parts[0] === "clip") return { provider: "twitch", id: parts[1] || "", kind: "clip" };
301
+ return { provider: "twitch", id: parts[0] || "", kind: "channel" };
302
+ }
303
+
304
+ function niconicoId(parsed) {
305
+ const parts = pathSegments(parsed);
306
+ if (parsed.hostname.endsWith("nico.ms")) return parts[0] || "";
307
+ if (parts[0] === "watch") return parts[1] || "";
308
+ return "";
309
+ }
310
+
311
+ function tiktokId(parsed) {
312
+ const parts = pathSegments(parsed);
313
+ const videoIndex = parts.indexOf("video");
314
+ return videoIndex >= 0 ? parts[videoIndex + 1] || "" : "";
315
+ }
316
+
317
+ function bilibiliId(parsed) {
318
+ const parts = pathSegments(parsed);
319
+ return parts.find((part) => /^BV/i.test(part) || /^av\d+/i.test(part)) || "";
320
+ }
321
+
322
+ function wistiaId(parsed) {
323
+ const parts = pathSegments(parsed);
324
+ const iframeIndex = parts.indexOf("iframe");
325
+ if (iframeIndex >= 0) return parts[iframeIndex + 1] || "";
326
+ return parts[parts.length - 1] || "";
327
+ }
328
+
329
+ function createParams(values) {
330
+ const params = new URLSearchParams();
331
+ Object.entries(values).forEach(([key, value]) => {
332
+ if (value === "" || value === false || value === null || value === undefined) return;
333
+ params.set(key, String(value));
334
+ });
335
+ const serialized = params.toString();
336
+ return serialized ? `?${serialized}` : "";
337
+ }
338
+
339
+ function currentHost() {
340
+ if (typeof window !== "undefined" && window.location && window.location.hostname) {
341
+ return window.location.hostname;
342
+ }
343
+ return "localhost";
344
+ }
345
+
346
+ function formatSize(value) {
347
+ return typeof value === "number" ? `${value}px` : value;
348
+ }
349
+
350
+ function providerLabel(value) {
351
+ const labels = {
352
+ youtube: "YouTube",
353
+ vimeo: "Vimeo",
354
+ dailymotion: "Dailymotion",
355
+ twitch: "Twitch",
356
+ niconico: "Niconico",
357
+ tiktok: "TikTok",
358
+ bilibili: "Bilibili",
359
+ wistia: "Wistia",
360
+ generic: "Embedded video"
361
+ };
362
+ return labels[value] || "Embedded video";
363
+ }
364
+ </script>
365
+
366
+ <style scoped>
367
+ .mikuru-embed-player {
368
+ display: grid;
369
+ gap: 9px;
370
+ margin: 0;
371
+ color: #0f172a;
372
+ font: inherit;
373
+ }
374
+
375
+ .embed-frame {
376
+ position: relative;
377
+ overflow: hidden;
378
+ width: 100%;
379
+ min-height: 160px;
380
+ border: 1px solid #cbd5e1;
381
+ border-radius: 8px;
382
+ background: #020617;
383
+ }
384
+
385
+ .embed-iframe,
386
+ .embed-empty {
387
+ position: absolute;
388
+ inset: 0;
389
+ width: 100%;
390
+ height: 100%;
391
+ }
392
+
393
+ .embed-iframe {
394
+ border: 0;
395
+ background: #020617;
396
+ }
397
+
398
+ .embed-empty {
399
+ display: grid;
400
+ place-content: center;
401
+ gap: 6px;
402
+ padding: 18px;
403
+ color: #e2e8f0;
404
+ text-align: center;
405
+ }
406
+
407
+ .embed-empty strong {
408
+ color: #ffffff;
409
+ font-size: 0.98rem;
410
+ }
411
+
412
+ .embed-empty span {
413
+ max-width: 34rem;
414
+ color: #cbd5e1;
415
+ font-size: 0.9rem;
416
+ }
417
+
418
+ .embed-caption {
419
+ display: grid;
420
+ gap: 2px;
421
+ color: #475569;
422
+ font-size: 0.9rem;
423
+ }
424
+
425
+ .embed-caption strong {
426
+ color: #0f172a;
427
+ font-size: 0.95rem;
428
+ }
429
+ </style>