@huyooo/file-explorer-preview 0.4.29 → 0.4.30

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.
@@ -0,0 +1,681 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
3
+ import { Icon } from '@iconify/vue';
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ url: string;
8
+ name: string;
9
+ /** 图片加载完成后的初始显示模式,来自用户偏好 */
10
+ initialImageFitMode?: 'fit' | 'actual';
11
+ }>(),
12
+ { initialImageFitMode: 'fit' },
13
+ );
14
+
15
+ const emit = defineEmits<{
16
+ loaded: [];
17
+ metadata: [value: { width: number; height: number }];
18
+ 'update:imageFitMode': [value: 'fit' | 'actual'];
19
+ }>();
20
+
21
+ const containerRef = ref<HTMLDivElement>();
22
+ const imgRef = ref<HTMLImageElement>();
23
+
24
+ const isLoading = ref(true);
25
+ const hasError = ref(false);
26
+ const isFullscreen = ref(false);
27
+ const showControls = ref(true);
28
+ let controlsTimer: number | undefined;
29
+
30
+ /**
31
+ * 视图模式:
32
+ * - 'fit' 适应窗口(缩放跟随容器尺寸,不可手动平移)
33
+ * - 'manual' 用户手动控制缩放/平移(包含「实际像素」、滚轮缩放、按钮缩放、拖拽)
34
+ */
35
+ type ViewMode = 'fit' | 'manual';
36
+ const viewMode = ref<ViewMode>('fit');
37
+ const scale = ref(1);
38
+ const rotation = ref(0);
39
+ const translateX = ref(0);
40
+ const translateY = ref(0);
41
+
42
+ /** 图片原始尺寸(不含 EXIF 旋转,浏览器已自动处理 orientation) */
43
+ const naturalWidth = ref(0);
44
+ const naturalHeight = ref(0);
45
+
46
+ /** 容器尺寸(由 ResizeObserver 更新) */
47
+ const containerWidth = ref(0);
48
+ const containerHeight = ref(0);
49
+
50
+ /** 拖拽状态 */
51
+ const isDragging = ref(false);
52
+ let dragStartX = 0;
53
+ let dragStartY = 0;
54
+ let dragStartTranslateX = 0;
55
+ let dragStartTranslateY = 0;
56
+
57
+ /** 双指缩放状态 */
58
+ let lastPinchDistance = 0;
59
+
60
+ /** 仅按钮/双击/快捷键触发时启用过渡 */
61
+ const useTransition = ref(false);
62
+
63
+ const MAX_SCALE = 10;
64
+ let resizeObserver: ResizeObserver | null = null;
65
+
66
+ /** 旋转后的图片包围盒尺寸(90/270 度时长宽互换) */
67
+ const rotatedBox = computed(() => {
68
+ const rotated = ((rotation.value % 180) + 180) % 180 === 90;
69
+ return {
70
+ width: rotated ? naturalHeight.value : naturalWidth.value,
71
+ height: rotated ? naturalWidth.value : naturalHeight.value,
72
+ };
73
+ });
74
+
75
+ /** 适应窗口缩放(同时考虑旋转后的包围盒) */
76
+ const fitScale = computed(() => {
77
+ const { width, height } = rotatedBox.value;
78
+ if (!width || !height || !containerWidth.value || !containerHeight.value) return 1;
79
+ return Math.min(containerWidth.value / width, containerHeight.value / height);
80
+ });
81
+
82
+ const minScale = computed(() => Math.max(0.1, fitScale.value * 0.5));
83
+
84
+ const isFitMode = computed(() => viewMode.value === 'fit');
85
+
86
+ /**
87
+ * 适合窗口:外层槽固定素材宽高比,浏览器在 max 约束下解「最大内接矩形」;
88
+ * 子图用 fill 铺满槽(槽与像素比一致,无拉伸),避免 100%×100%+contain 与亚像素容器失配出细边。
89
+ */
90
+ const fitSlotStyle = computed(() => {
91
+ const { width, height } = rotatedBox.value;
92
+ if (!width || !height) return undefined;
93
+ return { aspectRatio: `${width} / ${height}` };
94
+ });
95
+
96
+ const zoomPercent = computed(() => {
97
+ const s = viewMode.value === 'fit' ? fitScale.value : scale.value;
98
+ return Math.round(s * 100) + '%';
99
+ });
100
+ const toggleButtonText = computed(() => isFitMode.value ? '实际像素' : '适合窗口');
101
+
102
+ /** 手动模式:flex 居中 + 像素级 transform;适合窗口由 fit-slot + aspect-ratio 处理 */
103
+ const manualImageStyle = computed(() => {
104
+ if (viewMode.value === 'fit') return undefined;
105
+ if (!naturalWidth.value || !naturalHeight.value) return undefined;
106
+ return {
107
+ width: `${naturalWidth.value}px`,
108
+ height: `${naturalHeight.value}px`,
109
+ transform: `translate(${translateX.value}px, ${translateY.value}px) scale(${scale.value}) rotate(${rotation.value}deg)`,
110
+ transformOrigin: 'center center',
111
+ };
112
+ });
113
+
114
+ /** 进入手动模式时:保留当前视觉缩放、清零位移 */
115
+ function enterManualMode() {
116
+ if (viewMode.value === 'manual') return;
117
+ scale.value = fitScale.value;
118
+ translateX.value = 0;
119
+ translateY.value = 0;
120
+ viewMode.value = 'manual';
121
+ }
122
+
123
+ function fitToWindow() {
124
+ viewMode.value = 'fit';
125
+ scale.value = 1;
126
+ translateX.value = 0;
127
+ translateY.value = 0;
128
+ }
129
+
130
+ function fitToActual() {
131
+ viewMode.value = 'manual';
132
+ scale.value = 1;
133
+ translateX.value = 0;
134
+ translateY.value = 0;
135
+ }
136
+
137
+ /** 应用初始/同步的显示模式偏好 */
138
+ function applyInitialFitMode() {
139
+ if ((props.initialImageFitMode ?? 'fit') === 'actual') fitToActual();
140
+ else fitToWindow();
141
+ }
142
+
143
+ watch(() => props.initialImageFitMode, applyInitialFitMode);
144
+
145
+ function withTransition(fn: () => void) {
146
+ useTransition.value = true;
147
+ fn();
148
+ setTimeout(() => { useTransition.value = false; }, 250);
149
+ }
150
+
151
+ function toggleFitMode() {
152
+ withTransition(() => {
153
+ if (isFitMode.value) {
154
+ fitToActual();
155
+ emit('update:imageFitMode', 'actual');
156
+ } else {
157
+ fitToWindow();
158
+ emit('update:imageFitMode', 'fit');
159
+ }
160
+ });
161
+ }
162
+
163
+ function zoomIn() {
164
+ withTransition(() => {
165
+ enterManualMode();
166
+ scale.value = Math.min(MAX_SCALE, scale.value * 1.2);
167
+ });
168
+ }
169
+
170
+ function zoomOut() {
171
+ withTransition(() => {
172
+ enterManualMode();
173
+ scale.value = Math.max(minScale.value, scale.value / 1.2);
174
+ });
175
+ }
176
+
177
+ function rotateLeft() {
178
+ withTransition(() => {
179
+ if (viewMode.value === 'fit') enterManualMode();
180
+ rotation.value -= 90;
181
+ });
182
+ }
183
+
184
+ function rotateRight() {
185
+ withTransition(() => {
186
+ if (viewMode.value === 'fit') enterManualMode();
187
+ rotation.value += 90;
188
+ });
189
+ }
190
+
191
+ function resetView() {
192
+ withTransition(() => {
193
+ rotation.value = 0;
194
+ fitToWindow();
195
+ emit('update:imageFitMode', 'fit');
196
+ });
197
+ }
198
+
199
+ async function toggleFullscreen() {
200
+ if (!containerRef.value) return;
201
+ if (!document.fullscreenElement) await containerRef.value.requestFullscreen();
202
+ else await document.exitFullscreen();
203
+ }
204
+
205
+ /** 容器尺寸更新(ResizeObserver / 全屏切换都会调用) */
206
+ function updateContainerSize() {
207
+ if (!containerRef.value) return;
208
+ const rect = containerRef.value.getBoundingClientRect();
209
+ containerWidth.value = rect.width;
210
+ containerHeight.value = rect.height;
211
+ }
212
+
213
+ function handleFullscreenChange() {
214
+ isFullscreen.value = !!document.fullscreenElement;
215
+ updateContainerSize();
216
+ if (isFullscreen.value) resetControlsTimer();
217
+ else { showControls.value = true; clearControlsTimer(); }
218
+ }
219
+
220
+ function resetControlsTimer() {
221
+ showControls.value = true;
222
+ clearControlsTimer();
223
+ if (isFullscreen.value) {
224
+ controlsTimer = window.setTimeout(() => { showControls.value = false; }, 2000);
225
+ }
226
+ }
227
+
228
+ function clearControlsTimer() {
229
+ if (controlsTimer) { clearTimeout(controlsTimer); controlsTimer = undefined; }
230
+ }
231
+
232
+ function handleContainerMouseMove() {
233
+ if (isFullscreen.value) resetControlsTimer();
234
+ }
235
+
236
+ /**
237
+ * 滚轮缩放(参考 pixflow):
238
+ * - 无动画,直接更新;以鼠标位置为锚点;步长 5%
239
+ */
240
+ function handleWheel(e: WheelEvent) {
241
+ e.preventDefault();
242
+ if (!containerRef.value) return;
243
+ const rect = containerRef.value.getBoundingClientRect();
244
+ enterManualMode();
245
+
246
+ const mouseX = e.clientX - rect.left - rect.width / 2;
247
+ const mouseY = e.clientY - rect.top - rect.height / 2;
248
+
249
+ const oldScale = scale.value;
250
+ const direction = e.deltaY > 0 ? -1 : 1;
251
+ const step = oldScale * 0.05;
252
+ const newScale = Math.max(minScale.value, Math.min(MAX_SCALE, oldScale + direction * step));
253
+
254
+ if (Math.abs(newScale - oldScale) < 0.001) return;
255
+
256
+ const ratio = newScale / oldScale;
257
+ translateX.value = mouseX - (mouseX - translateX.value) * ratio;
258
+ translateY.value = mouseY - (mouseY - translateY.value) * ratio;
259
+ scale.value = newScale;
260
+ }
261
+
262
+ function handleMouseDown(e: MouseEvent) {
263
+ if ((e.target as HTMLElement).closest('.toolbar')) return;
264
+ isDragging.value = true;
265
+ dragStartX = e.clientX;
266
+ dragStartY = e.clientY;
267
+ dragStartTranslateX = translateX.value;
268
+ dragStartTranslateY = translateY.value;
269
+ e.preventDefault();
270
+ }
271
+
272
+ function handleMouseMove(e: MouseEvent) {
273
+ if (!isDragging.value) return;
274
+ enterManualMode();
275
+ translateX.value = dragStartTranslateX + e.clientX - dragStartX;
276
+ translateY.value = dragStartTranslateY + e.clientY - dragStartY;
277
+ }
278
+
279
+ function handleMouseUp() { isDragging.value = false; }
280
+
281
+ function handleTouchStart(e: TouchEvent) {
282
+ if ((e.target as HTMLElement).closest('.toolbar')) return;
283
+ if (e.touches.length === 1) {
284
+ isDragging.value = true;
285
+ dragStartX = e.touches[0].clientX;
286
+ dragStartY = e.touches[0].clientY;
287
+ dragStartTranslateX = translateX.value;
288
+ dragStartTranslateY = translateY.value;
289
+ } else if (e.touches.length === 2) {
290
+ lastPinchDistance = getPinchDistance(e.touches);
291
+ }
292
+ }
293
+
294
+ function handleTouchMove(e: TouchEvent) {
295
+ e.preventDefault();
296
+ if (e.touches.length === 1 && isDragging.value) {
297
+ enterManualMode();
298
+ translateX.value = dragStartTranslateX + e.touches[0].clientX - dragStartX;
299
+ translateY.value = dragStartTranslateY + e.touches[0].clientY - dragStartY;
300
+ } else if (e.touches.length === 2) {
301
+ enterManualMode();
302
+ const distance = getPinchDistance(e.touches);
303
+ if (lastPinchDistance > 0) {
304
+ const ratio = distance / lastPinchDistance;
305
+ scale.value = Math.min(Math.max(scale.value * ratio, minScale.value), MAX_SCALE);
306
+ }
307
+ lastPinchDistance = distance;
308
+ }
309
+ }
310
+
311
+ function handleTouchEnd() { isDragging.value = false; lastPinchDistance = 0; }
312
+
313
+ function getPinchDistance(touches: TouchList): number {
314
+ const dx = touches[0].clientX - touches[1].clientX;
315
+ const dy = touches[0].clientY - touches[1].clientY;
316
+ return Math.sqrt(dx * dx + dy * dy);
317
+ }
318
+
319
+ function handleDoubleClick(e: MouseEvent) {
320
+ if ((e.target as HTMLElement).closest('.toolbar')) return;
321
+ toggleFullscreen();
322
+ }
323
+
324
+ function handleKeyDown(e: KeyboardEvent) {
325
+ switch (e.key) {
326
+ case '+': case '=': zoomIn(); break;
327
+ case '-': zoomOut(); break;
328
+ case '1': withTransition(() => { fitToActual(); emit('update:imageFitMode', 'actual'); }); break;
329
+ case '0': withTransition(() => { fitToWindow(); emit('update:imageFitMode', 'fit'); }); break;
330
+ case 'f': case 'F': toggleFullscreen(); break;
331
+ case 'Escape': if (isFullscreen.value) { e.preventDefault(); document.exitFullscreen(); } break;
332
+ case '[': rotateLeft(); break;
333
+ case ']': rotateRight(); break;
334
+ case 'r': case 'R': resetView(); break;
335
+ case ' ': e.preventDefault(); toggleFitMode(); break;
336
+ }
337
+ }
338
+
339
+ function handleImageLoad() {
340
+ if (!imgRef.value) return;
341
+ naturalWidth.value = imgRef.value.naturalWidth;
342
+ naturalHeight.value = imgRef.value.naturalHeight;
343
+ emit('metadata', { width: naturalWidth.value, height: naturalHeight.value });
344
+ isLoading.value = false;
345
+ updateContainerSize();
346
+ emit('loaded');
347
+ applyInitialFitMode();
348
+ }
349
+
350
+ function handleImageError() {
351
+ isLoading.value = false;
352
+ hasError.value = true;
353
+ emit('loaded');
354
+ }
355
+
356
+ onMounted(() => {
357
+ updateContainerSize();
358
+ if (containerRef.value) {
359
+ resizeObserver = new ResizeObserver(updateContainerSize);
360
+ resizeObserver.observe(containerRef.value);
361
+ }
362
+ document.addEventListener('keydown', handleKeyDown);
363
+ document.addEventListener('mousemove', handleMouseMove);
364
+ document.addEventListener('mouseup', handleMouseUp);
365
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
366
+ });
367
+
368
+ onUnmounted(() => {
369
+ resizeObserver?.disconnect();
370
+ resizeObserver = null;
371
+ clearControlsTimer();
372
+ document.removeEventListener('keydown', handleKeyDown);
373
+ document.removeEventListener('mousemove', handleMouseMove);
374
+ document.removeEventListener('mouseup', handleMouseUp);
375
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
376
+ });
377
+ </script>
378
+
379
+ <template>
380
+ <div
381
+ ref="containerRef"
382
+ class="viewer"
383
+ :class="{
384
+ dragging: isDragging,
385
+ animated: useTransition,
386
+ fullscreen: isFullscreen,
387
+ 'hide-controls': isFullscreen && !showControls,
388
+ }"
389
+ @wheel.prevent="handleWheel"
390
+ @mousedown="handleMouseDown"
391
+ @mousemove="handleContainerMouseMove"
392
+ @touchstart="handleTouchStart"
393
+ @touchmove.prevent="handleTouchMove"
394
+ @touchend="handleTouchEnd"
395
+ @dblclick="handleDoubleClick"
396
+ >
397
+ <div v-if="isLoading" class="loading">
398
+ <div class="spinner"></div>
399
+ <span>加载中...</span>
400
+ </div>
401
+
402
+ <div v-else-if="hasError" class="error">
403
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
404
+ <rect x="3" y="3" width="18" height="18" rx="2" />
405
+ <circle cx="8.5" cy="8.5" r="1.5" />
406
+ <path d="M21 15l-5-5L5 21" />
407
+ </svg>
408
+ <p>无法加载图片</p>
409
+ </div>
410
+
411
+ <div v-show="!isLoading && !hasError" class="media-stage">
412
+ <div
413
+ class="slot-root"
414
+ :class="isFitMode ? 'slot-root--fit' : 'slot-root--manual'"
415
+ :style="isFitMode ? fitSlotStyle : undefined"
416
+ >
417
+ <img
418
+ ref="imgRef"
419
+ class="image"
420
+ :class="isFitMode ? 'image--fit-slot' : 'image--manual'"
421
+ :src="url"
422
+ :alt="name"
423
+ :style="manualImageStyle"
424
+ draggable="false"
425
+ @load="handleImageLoad"
426
+ @error="handleImageError"
427
+ />
428
+ </div>
429
+ </div>
430
+
431
+ <div v-if="!isLoading && !hasError" class="toolbar">
432
+ <button @click="zoomOut" title="缩小 (-)">
433
+ <Icon icon="lucide:zoom-out" />
434
+ </button>
435
+ <span class="zoom-label">{{ zoomPercent }}</span>
436
+ <button @click="zoomIn" title="放大 (+)">
437
+ <Icon icon="lucide:zoom-in" />
438
+ </button>
439
+ <div class="divider"></div>
440
+ <button @click="rotateLeft" title="逆时针旋转 ([)">
441
+ <Icon icon="lucide:rotate-ccw" />
442
+ </button>
443
+ <button @click="rotateRight" title="顺时针旋转 (])">
444
+ <Icon icon="lucide:rotate-cw" />
445
+ </button>
446
+ <div class="divider"></div>
447
+ <button class="fit-toggle" @click="toggleFitMode" :title="`${toggleButtonText} (空格)`">
448
+ <Icon v-if="isFitMode" icon="lucide:maximize-2" />
449
+ <Icon v-else icon="lucide:minimize-2" />
450
+ <span>{{ toggleButtonText }}</span>
451
+ </button>
452
+ <div class="divider"></div>
453
+ <button @click="resetView" title="重置 (R)">
454
+ <Icon icon="lucide:refresh-ccw" />
455
+ </button>
456
+ <button @click="toggleFullscreen" :title="isFullscreen ? '退出全屏 (F)' : '全屏 (F)'">
457
+ <Icon v-if="isFullscreen" icon="lucide:minimize-2" />
458
+ <Icon v-else icon="lucide:fullscreen" />
459
+ </button>
460
+ </div>
461
+ </div>
462
+ </template>
463
+
464
+ <style scoped>
465
+ .viewer {
466
+ width: 100%;
467
+ height: 100%;
468
+ min-width: 0;
469
+ min-height: 0;
470
+ position: relative;
471
+ background: #000;
472
+ cursor: grab;
473
+ overflow: hidden;
474
+ }
475
+
476
+ .viewer.dragging {
477
+ cursor: grabbing;
478
+ }
479
+
480
+ .viewer.fullscreen {
481
+ position: fixed;
482
+ inset: 0;
483
+ z-index: 9999;
484
+ }
485
+
486
+ .viewer.fullscreen.hide-controls {
487
+ cursor: none;
488
+ }
489
+
490
+ .viewer.fullscreen.hide-controls .toolbar {
491
+ opacity: 0 !important;
492
+ pointer-events: none;
493
+ }
494
+
495
+ .loading,
496
+ .error {
497
+ position: absolute;
498
+ inset: 0;
499
+ z-index: 2;
500
+ display: flex;
501
+ flex-direction: column;
502
+ align-items: center;
503
+ justify-content: center;
504
+ gap: 16px;
505
+ }
506
+
507
+ .loading {
508
+ color: var(--huyooo-text-muted);
509
+ font-size: 14px;
510
+ }
511
+
512
+ .spinner {
513
+ width: 40px;
514
+ height: 40px;
515
+ border: 3px solid color-mix(in srgb, var(--huyooo-text) 10%, transparent);
516
+ border-top-color: var(--huyooo-text);
517
+ border-radius: 50%;
518
+ animation: spin 1s linear infinite;
519
+ }
520
+
521
+ @keyframes spin {
522
+ to { transform: rotate(360deg); }
523
+ }
524
+
525
+ .error svg {
526
+ width: 48px;
527
+ height: 48px;
528
+ color: var(--huyooo-danger);
529
+ }
530
+
531
+ .error p {
532
+ color: var(--huyooo-text);
533
+ font-size: 14px;
534
+ }
535
+
536
+ /* 与 VideoPlayer 一致:整块舞台铺满;适合窗口时子槽用 aspect-ratio 由浏览器解「最大内接矩形」 */
537
+ .media-stage {
538
+ position: absolute;
539
+ inset: 0;
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: center;
543
+ overflow: hidden;
544
+ }
545
+
546
+ .slot-root--fit {
547
+ margin: auto;
548
+ max-width: 100%;
549
+ max-height: 100%;
550
+ min-width: 0;
551
+ min-height: 0;
552
+ width: auto;
553
+ height: auto;
554
+ box-sizing: border-box;
555
+ }
556
+
557
+ .slot-root--manual {
558
+ width: 100%;
559
+ height: 100%;
560
+ display: flex;
561
+ align-items: center;
562
+ justify-content: center;
563
+ }
564
+
565
+ .image {
566
+ user-select: none;
567
+ -webkit-user-drag: none;
568
+ box-shadow: var(--huyooo-shadow-lg);
569
+ }
570
+
571
+ /* 槽与像素比一致,子图铺满槽 → 无 contain 在错比例矩形里的细边 */
572
+ .image.image--fit-slot {
573
+ width: 100%;
574
+ height: 100%;
575
+ object-fit: fill;
576
+ display: block;
577
+ }
578
+
579
+ .image.image--manual {
580
+ max-width: none;
581
+ max-height: none;
582
+ flex-shrink: 0;
583
+ will-change: transform;
584
+ }
585
+
586
+ .viewer.animated .image.image--manual {
587
+ transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
588
+ }
589
+
590
+ /*
591
+ * 工具栏:fit-content 胶囊,水平居中浮在图片底部(macOS Preview 风格)。
592
+ * position: absolute + 仅指定 left(不指定 right)→ 元素宽度自然 shrink-to-fit;
593
+ * inline-flex 让内部按内容布局;transform translateX(-50%) 把胶囊中心对齐到容器中心。
594
+ */
595
+ .toolbar {
596
+ position: absolute;
597
+ bottom: 16px;
598
+ left: 50%;
599
+ z-index: 3;
600
+ display: inline-flex;
601
+ align-items: center;
602
+ gap: 4px;
603
+ padding: 8px 12px;
604
+ background: var(--huyooo-panel-bg);
605
+ backdrop-filter: blur(20px);
606
+ border-radius: 12px;
607
+ border: 1px solid var(--huyooo-border);
608
+ box-shadow: var(--huyooo-shadow-lg);
609
+ opacity: 0;
610
+ transform: translate(-50%, 8px);
611
+ transition: opacity 0.25s, transform 0.25s;
612
+ -webkit-app-region: no-drag;
613
+ }
614
+
615
+ .viewer:hover .toolbar,
616
+ .viewer.fullscreen .toolbar {
617
+ opacity: 1;
618
+ transform: translate(-50%, 0);
619
+ }
620
+
621
+ .toolbar button {
622
+ width: 32px;
623
+ height: 32px;
624
+ border: none;
625
+ background: transparent;
626
+ border-radius: 6px;
627
+ cursor: pointer;
628
+ display: flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ color: var(--huyooo-text);
632
+ transition: all 0.15s;
633
+ }
634
+
635
+ .toolbar button:hover {
636
+ background: var(--huyooo-muted-hover);
637
+ color: var(--huyooo-on-primary, #ffffff);
638
+ }
639
+
640
+ .toolbar button:active {
641
+ transform: scale(0.95);
642
+ }
643
+
644
+ .toolbar button svg,
645
+ .toolbar button :deep(svg) {
646
+ width: 18px;
647
+ height: 18px;
648
+ }
649
+
650
+ .toolbar button :deep(.iconify) {
651
+ font-size: 18px;
652
+ }
653
+
654
+ .toolbar .divider {
655
+ width: 1px;
656
+ height: 20px;
657
+ background: var(--huyooo-border);
658
+ margin: 0 6px;
659
+ }
660
+
661
+ .zoom-label {
662
+ color: var(--huyooo-text-muted);
663
+ font-size: 11px;
664
+ font-weight: 500;
665
+ min-width: 42px;
666
+ text-align: center;
667
+ font-variant-numeric: tabular-nums;
668
+ }
669
+
670
+ .fit-toggle {
671
+ width: auto !important;
672
+ min-width: 80px;
673
+ padding: 0 10px !important;
674
+ gap: 5px;
675
+ }
676
+
677
+ .fit-toggle span {
678
+ font-size: 11px;
679
+ white-space: nowrap;
680
+ }
681
+ </style>