@opendesign-plus-test/components 0.0.1-rc.43 → 0.0.1-rc.45

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 (83) hide show
  1. package/dist/chunk-OElCookieNotice.cjs.js +1 -1
  2. package/dist/chunk-OElCookieNotice.es.js +67 -47
  3. package/dist/components/OHeaderSearch.vue.d.ts +822 -500
  4. package/dist/components/activity/OMyActivityCalendar.vue.d.ts +86 -24
  5. package/dist/components/activity/index.d.ts +43 -12
  6. package/dist/components/meeting/OMyMeetingCalendar.vue.d.ts +86 -24
  7. package/dist/components/meeting/index.d.ts +43 -12
  8. package/dist/components/search/OSearchInput.vue.d.ts +1003 -0
  9. package/dist/components/search/composables/useImageSearch.d.ts +48 -0
  10. package/dist/components/search/composables/useKeywordHighlight.d.ts +2 -0
  11. package/dist/components/search/composables/useSearchHistory.d.ts +14 -0
  12. package/dist/components/search/index.d.ts +590 -0
  13. package/dist/components/search/internal/HighlightText.vue.d.ts +9 -0
  14. package/dist/components/search/internal/SearchImageInput.vue.d.ts +716 -0
  15. package/dist/components/search/internal/SearchPanel.vue.d.ts +100 -0
  16. package/dist/components/search/types.d.ts +20 -0
  17. package/dist/components.cjs.js +41 -41
  18. package/dist/components.css +1 -1
  19. package/dist/components.es.js +11228 -10253
  20. package/dist/index.d.ts +1 -0
  21. package/package.json +3 -3
  22. package/src/assets/svg-icons/icon-delete-hover.svg +4 -0
  23. package/src/assets/svg-icons/icon-delete.svg +5 -1
  24. package/src/assets/svg-icons/icon-image-close.svg +4 -0
  25. package/src/assets/svg-icons/icon-image-upload.svg +3 -0
  26. package/src/assets/svg-icons/icon-image-zoomin.svg +3 -0
  27. package/src/assets/svg-icons/icon-refresh.svg +3 -0
  28. package/src/components/OBanner.vue +18 -18
  29. package/src/components/OCookieNotice.vue +21 -21
  30. package/src/components/OFooter.vue +18 -17
  31. package/src/components/OHeaderSearch.vue +402 -420
  32. package/src/components/OHeaderUser.vue +3 -2
  33. package/src/components/OSection.vue +4 -4
  34. package/src/components/activity/OActivityApproval.vue +4 -4
  35. package/src/components/activity/OActivityForm.vue +2 -2
  36. package/src/components/activity/OMyActivityCalendar.vue +26 -26
  37. package/src/components/activity/config.ts +1 -1
  38. package/src/components/common/ContentWrapper.vue +3 -3
  39. package/src/components/element-plus/OElCookieNotice.vue +26 -26
  40. package/src/components/events/OEventsApply.vue +44 -44
  41. package/src/components/events/OEventsCalendar.vue +14 -14
  42. package/src/components/events/OEventsList.vue +16 -16
  43. package/src/components/header/OHeader.vue +2 -2
  44. package/src/components/header/components/HeaderContent.vue +60 -60
  45. package/src/components/header/components/HeaderNav.vue +4 -4
  46. package/src/components/header/components/HeaderNavMobile.vue +3 -3
  47. package/src/components/meeting/OMeetingCalendar.vue +27 -27
  48. package/src/components/meeting/OMeetingForm.vue +16 -16
  49. package/src/components/meeting/OMeetingPlayback.vue +4 -4
  50. package/src/components/meeting/OMyMeetingCalendar.vue +25 -25
  51. package/src/components/meeting/OSigMeetingCalendar.vue +3 -3
  52. package/src/components/meeting/components/OMeetingCalendarList.vue +9 -9
  53. package/src/components/meeting/components/OMeetingDetail.vue +2 -2
  54. package/src/components/meeting/components/OMeetingPlaybackSubtitles.vue +1 -1
  55. package/src/components/meeting/components/OMeetingPlaybackVideo.vue +5 -5
  56. package/src/components/meeting/components/OSigMeetingAside.vue +6 -6
  57. package/src/components/search/OSearchInput.vue +463 -0
  58. package/src/components/search/composables/useImageSearch.ts +157 -0
  59. package/src/components/search/composables/useKeywordHighlight.ts +30 -0
  60. package/src/components/search/composables/useSearchHistory.ts +75 -0
  61. package/src/components/search/index.ts +23 -0
  62. package/src/components/search/internal/HighlightText.vue +37 -0
  63. package/src/components/search/internal/SearchImageInput.vue +488 -0
  64. package/src/components/search/internal/SearchPanel.vue +430 -0
  65. package/src/components/search/types.ts +25 -0
  66. package/src/draft/Banner.vue +6 -6
  67. package/src/draft/ButtonCards.vue +1 -1
  68. package/src/draft/Feature.vue +6 -6
  69. package/src/draft/Footer.vue +29 -22
  70. package/src/draft/HorizontalAnchor.vue +4 -4
  71. package/src/draft/ItemSwiper.vue +2 -2
  72. package/src/draft/Logo.vue +3 -3
  73. package/src/draft/LogoCard.vue +2 -2
  74. package/src/draft/MultiCard.vue +1 -1
  75. package/src/draft/MultiIconCard.vue +1 -1
  76. package/src/draft/OInfoCard.vue +4 -4
  77. package/src/draft/Section.vue +4 -4
  78. package/src/draft/SingleTabCard.vue +1 -1
  79. package/src/draft/SliderCard.vue +4 -3
  80. package/src/i18n/en.ts +10 -0
  81. package/src/i18n/zh.ts +10 -0
  82. package/src/index.ts +1 -0
  83. package/vite.config.ts +1 -1
@@ -0,0 +1,30 @@
1
+ import type { OSearchHighlightPart } from '../types';
2
+
3
+ const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g;
4
+
5
+ export function highlightKeywordParts(text: string, keyword: string): OSearchHighlightPart[] {
6
+ const k = (keyword || '').trim();
7
+ if (!k) return [{ text, match: false }];
8
+
9
+ const escaped = k.replace(ESCAPE_RE, '\\$&');
10
+ const re = new RegExp(escaped, 'gi');
11
+ const parts: OSearchHighlightPart[] = [];
12
+ let lastIndex = 0;
13
+ let m: RegExpExecArray | null;
14
+
15
+ while ((m = re.exec(text)) !== null) {
16
+ if (m.index === re.lastIndex) {
17
+ re.lastIndex++;
18
+ continue;
19
+ }
20
+ if (m.index > lastIndex) {
21
+ parts.push({ text: text.slice(lastIndex, m.index), match: false });
22
+ }
23
+ parts.push({ text: m[0], match: true });
24
+ lastIndex = m.index + m[0].length;
25
+ }
26
+ if (lastIndex < text.length) {
27
+ parts.push({ text: text.slice(lastIndex), match: false });
28
+ }
29
+ return parts;
30
+ }
@@ -0,0 +1,75 @@
1
+ import { ref, watch, onMounted, type Ref } from 'vue';
2
+
3
+ export interface UseSearchHistoryOptions {
4
+ initial: Ref<string[]>;
5
+ storageKey: Ref<string>;
6
+ storeHistory: Ref<boolean>;
7
+ maxHistoryCount: Ref<number>;
8
+ onChange?: (items: string[]) => void;
9
+ }
10
+
11
+ export function useSearchHistory(options: UseSearchHistoryOptions) {
12
+ const { initial, storageKey, storeHistory, maxHistoryCount, onChange } = options;
13
+ const items = ref<string[]>([...initial.value]);
14
+
15
+ const persist = () => {
16
+ if (!storeHistory.value || typeof localStorage === 'undefined') return;
17
+ try {
18
+ if (items.value.length) {
19
+ localStorage.setItem(storageKey.value, JSON.stringify(items.value));
20
+ } else {
21
+ localStorage.removeItem(storageKey.value);
22
+ }
23
+ } catch {
24
+ // ignore
25
+ }
26
+ };
27
+
28
+ const setItems = (next: string[]) => {
29
+ items.value = next;
30
+ persist();
31
+ onChange?.(items.value);
32
+ };
33
+
34
+ const loadFromStorage = () => {
35
+ if (!storeHistory.value || typeof localStorage === 'undefined') return;
36
+ try {
37
+ const raw = localStorage.getItem(storageKey.value);
38
+ if (!raw) return;
39
+ const parsed = JSON.parse(raw);
40
+ if (Array.isArray(parsed) && parsed.length) {
41
+ const merged = Array.from(new Set<string>([...parsed, ...items.value]));
42
+ items.value = merged.slice(0, maxHistoryCount.value);
43
+ onChange?.(items.value);
44
+ }
45
+ } catch {
46
+ // ignore
47
+ }
48
+ };
49
+
50
+ const push = (val: string) => {
51
+ const v = val?.trim();
52
+ if (!v) return;
53
+ const next = [v, ...items.value.filter((i) => i !== v)];
54
+ if (next.length > maxHistoryCount.value) next.length = maxHistoryCount.value;
55
+ setItems(next);
56
+ };
57
+
58
+ const remove = (val: string) => {
59
+ setItems(items.value.filter((i) => i !== val));
60
+ };
61
+
62
+ const clearAll = () => {
63
+ setItems([]);
64
+ };
65
+
66
+ watch(initial, (val) => {
67
+ if (val !== items.value) {
68
+ items.value = [...val];
69
+ }
70
+ });
71
+
72
+ onMounted(loadFromStorage);
73
+
74
+ return { items, push, remove, clearAll };
75
+ }
@@ -0,0 +1,23 @@
1
+ import _OSearchInput from './OSearchInput.vue';
2
+ import type { App } from 'vue';
3
+
4
+ const OSearchInput = Object.assign(_OSearchInput, {
5
+ install(app: App) {
6
+ app.component('OSearchInput', _OSearchInput);
7
+ },
8
+ });
9
+
10
+ export { OSearchInput };
11
+
12
+ export type {
13
+ OSearchRecommendItem,
14
+ OSearchPayload,
15
+ OSearchImageUploadErrorPayload,
16
+ OSearchUploadImageFn,
17
+ OSearchItemClickType,
18
+ OSearchHighlightPart,
19
+ } from './types';
20
+
21
+ // Public prop type aliases (re-exported from .vue SFCs).
22
+ // Vue SFC type shims don't always re-export named types, so consumers should rely on
23
+ // ComponentProps<typeof Component> if they need props typing.
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { highlightKeywordParts } from '../composables/useKeywordHighlight';
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ text: string;
8
+ keyword: string;
9
+ enabled?: boolean;
10
+ }>(),
11
+ {
12
+ enabled: true,
13
+ }
14
+ );
15
+
16
+ const parts = computed(() => {
17
+ if (!props.enabled || !props.keyword) {
18
+ return [{ text: props.text, match: false }];
19
+ }
20
+ return highlightKeywordParts(props.text, props.keyword);
21
+ });
22
+ </script>
23
+
24
+ <template>
25
+ <span class="o-search-highlight-text">
26
+ <span v-for="(p, idx) in parts" :key="idx" :class="{ 'is-match': p.match }">{{ p.text }}</span>
27
+ </span>
28
+ </template>
29
+
30
+ <style lang="scss" scoped>
31
+ .o-search-highlight-text {
32
+ .is-match {
33
+ color: var(--o-color-primary1);
34
+ font-weight: 600;
35
+ }
36
+ }
37
+ </style>
@@ -0,0 +1,488 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref, watch } from 'vue';
3
+ import { OFigure, OIcon, OInput, OPopover } from '@opensig/opendesign';
4
+ import { useImageSearch } from '../composables/useImageSearch';
5
+ import { useI18n } from '@/i18n';
6
+ import type { OSearchUploadImageFn } from '../types';
7
+
8
+ import IconSearch from '~icons/components/icon-header-search.svg';
9
+ import IconClose from '~icons/components/icon-close.svg';
10
+ import IconImageUpload from '~icons/components/icon-image-upload.svg';
11
+ import IconImageClose from '~icons/components/icon-delete-hover.svg';
12
+ import IconImageZoomin from '~icons/components/icon-image-zoomin.svg';
13
+
14
+ export interface SearchImageInputPropsT {
15
+ modelValue?: string;
16
+ imageUrl?: string;
17
+ placeholder?: string;
18
+ imagePlaceholder?: string;
19
+ size?: 'small' | 'medium' | 'large';
20
+ enableImageSearch?: boolean;
21
+ uploadImage?: OSearchUploadImageFn;
22
+ maxImageSize?: number;
23
+ imageUploadTooltip?: string;
24
+ /** Show preview block below input when true and image staged */
25
+ expanded?: boolean;
26
+ /** Whether to render the clear icon when there is content */
27
+ clearable?: boolean;
28
+ /** Show the right suffix area (upload + clear). When false, suffix is empty. */
29
+ showSuffix?: boolean;
30
+ /** When true, render thumbnail inline inside prefix area (compact mode) */
31
+ inlineThumbnail?: boolean;
32
+ preview?: boolean;
33
+ disabled?: boolean;
34
+ allowedImageTypes?: string[];
35
+ }
36
+
37
+ const props = withDefaults(defineProps<SearchImageInputPropsT>(), {
38
+ modelValue: '',
39
+ imageUrl: '',
40
+ size: 'medium',
41
+ enableImageSearch: false,
42
+ maxImageSize: 10 * 1024 * 1024,
43
+ expanded: true,
44
+ clearable: true,
45
+ showSuffix: true,
46
+ inlineThumbnail: false,
47
+ preview: true,
48
+ disabled: false,
49
+ allowedImageTypes: () => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
50
+ });
51
+
52
+ const emit = defineEmits<{
53
+ (e: 'update:modelValue', val: string): void;
54
+ (e: 'update:imageUrl', url: string): void;
55
+ (e: 'focus', ev: FocusEvent): void;
56
+ (e: 'blur', ev: FocusEvent): void;
57
+ (e: 'enter'): void;
58
+ (e: 'clear'): void;
59
+ (e: 'image-clear'): void;
60
+ (e: 'image-upload-start', file: File): void;
61
+ (e: 'image-upload-success', url: string, file: File): void;
62
+ (e: 'image-upload-error', error: unknown, file: File): void;
63
+ (e: 'image-validate-error', reason: 'size' | 'type', file: File): void;
64
+ (e: 'preview-change', visible: boolean): void;
65
+ }>();
66
+
67
+ const { t } = useI18n();
68
+
69
+ const uploadImageRef = computed(() => props.uploadImage);
70
+ const maxImageSizeRef = computed(() => props.maxImageSize);
71
+ const allowedImageTypesRef = computed(() => props.allowedImageTypes);
72
+ const acceptAttr = computed(() => (props.allowedImageTypes ?? []).join(','));
73
+
74
+ const image = useImageSearch({
75
+ uploadImage: uploadImageRef,
76
+ maxImageSize: maxImageSizeRef,
77
+ allowedImageTypes: allowedImageTypesRef,
78
+ onUploadStart: (file) => emit('image-upload-start', file),
79
+ onUploadSuccess: (url, file) => {
80
+ emit('update:imageUrl', url);
81
+ emit('image-upload-success', url, file);
82
+ },
83
+ onUploadError: (error, file) => emit('image-upload-error', error, file),
84
+ onValidationError: (reason, file) => emit('image-validate-error', reason, file),
85
+ });
86
+
87
+ const fileInputRef = ref<HTMLInputElement>();
88
+ const inputRef = ref<InstanceType<typeof OInput>>();
89
+ const uploadBtnRef = ref<HTMLElement | null>(null);
90
+
91
+ const innerValue = computed({
92
+ get: () => props.modelValue,
93
+ set: (val: string) => emit('update:modelValue', val),
94
+ });
95
+
96
+ watch(
97
+ () => props.imageUrl,
98
+ (val) => {
99
+ image.setExternalUrl(val);
100
+ },
101
+ { immediate: true }
102
+ );
103
+
104
+ const showThumbnail = computed(() => !!image.previewUrl.value);
105
+
106
+ const placeholder = computed(() => {
107
+ if (showThumbnail.value) {
108
+ return props.imagePlaceholder ?? t('search.extendedPlaceholder');
109
+ }
110
+ return props.placeholder ?? t('search.placeholder');
111
+ });
112
+
113
+ const handleClear = () => {
114
+ innerValue.value = '';
115
+ if (showThumbnail.value) {
116
+ handleImageClear();
117
+ }
118
+ emit('clear');
119
+ };
120
+
121
+ const handleImageClear = () => {
122
+ image.reset();
123
+ emit('update:imageUrl', '');
124
+ emit('image-clear');
125
+ };
126
+
127
+ const handleUploadClick = () => {
128
+ image.pickFile(fileInputRef.value);
129
+ nextTick(() => inputRef.value?.focus?.());
130
+ };
131
+
132
+ const handleFileSelect = (event: Event) => {
133
+ image.onFileChange(event);
134
+ nextTick(() => inputRef.value?.focus?.());
135
+ };
136
+
137
+ const handlePaste = (event: ClipboardEvent) => {
138
+ if (!props.enableImageSearch) return;
139
+ image.onPaste(event);
140
+ };
141
+
142
+ const handleDragOver = (event: DragEvent) => {
143
+ if (!props.enableImageSearch) return;
144
+ image.onDragOver(event);
145
+ };
146
+
147
+ const handleDrop = (event: DragEvent) => {
148
+ if (!props.enableImageSearch) return;
149
+ image.onDrop(event);
150
+ };
151
+
152
+ const onPreviewChange = (visible: boolean) => {
153
+ emit('preview-change', visible);
154
+ };
155
+
156
+ const hasValue = computed(() => !!innerValue.value || showThumbnail.value);
157
+
158
+ const awaitUpload = () => image.awaitUpload();
159
+
160
+ defineExpose({
161
+ focus: () => inputRef.value?.focus?.(),
162
+ blur: () => inputRef.value?.blur?.(),
163
+ awaitUpload,
164
+ getUploadedUrl: () => image.uploadedUrl.value,
165
+ getIsUploading: () => image.isUploading.value,
166
+ resetImage: () => {
167
+ image.reset();
168
+ emit('update:imageUrl', '');
169
+ },
170
+ });
171
+ </script>
172
+
173
+ <template>
174
+ <div
175
+ class="o-search-image-input"
176
+ :class="{ 'has-image': showThumbnail, 'is-expanded': expanded && showThumbnail }"
177
+ @dragover="handleDragOver"
178
+ @drop="handleDrop"
179
+ >
180
+ <div class="o-search-image-input-row">
181
+ <OInput
182
+ ref="inputRef"
183
+ v-model="innerValue"
184
+ :placeholder="placeholder"
185
+ :size="size"
186
+ :disabled="disabled"
187
+ class="o-search-image-input-input"
188
+ @pressEnter="emit('enter')"
189
+ @focus="(e: FocusEvent) => emit('focus', e)"
190
+ @blur="(e: FocusEvent) => emit('blur', e)"
191
+ @paste="handlePaste"
192
+ >
193
+ <template #prefix>
194
+ <slot name="prefix">
195
+ <OIcon class="o-search-image-input-icon">
196
+ <IconSearch />
197
+ </OIcon>
198
+ </slot>
199
+ <div v-if="inlineThumbnail && showThumbnail" class="o-search-image-input-thumb">
200
+ <OFigure :src="image.previewUrl.value" alt="" class="o-search-image-input-thumb-figure" />
201
+ <div class="o-search-image-input-thumb-zoom" @mousedown.prevent>
202
+ <OIcon class="o-search-image-input-thumb-zoom-icon"><IconImageZoomin /></OIcon>
203
+ </div>
204
+ <OIcon class="o-search-image-input-thumb-remove" @mousedown.prevent @click.stop="handleImageClear">
205
+ <IconImageClose />
206
+ </OIcon>
207
+ </div>
208
+ </template>
209
+
210
+ <template v-if="showSuffix" #suffix>
211
+ <slot name="suffix" :has-value="hasValue">
212
+ <span v-if="enableImageSearch" ref="uploadBtnRef" class="o-search-image-input-upload-btn">
213
+ <OIcon class="o-search-image-input-icon upload" @click.stop="handleUploadClick">
214
+ <IconImageUpload />
215
+ </OIcon>
216
+ </span>
217
+ <OPopover
218
+ v-if="enableImageSearch && uploadBtnRef"
219
+ trigger="hover"
220
+ position="bottom"
221
+ :target="uploadBtnRef"
222
+ body-class="o-search-image-input-tooltip"
223
+ >
224
+ {{ imageUploadTooltip ?? t('search.imageUploadTooltip') }}
225
+ </OPopover>
226
+ <OIcon
227
+ v-if="clearable && hasValue"
228
+ class="o-search-image-input-icon close"
229
+ @mousedown.prevent
230
+ @click.stop="handleClear"
231
+ >
232
+ <IconClose />
233
+ </OIcon>
234
+ </slot>
235
+ </template>
236
+ </OInput>
237
+ </div>
238
+
239
+ <div v-if="expanded && showThumbnail" class="o-search-image-input-preview">
240
+ <slot name="preview" :preview-url="image.previewUrl.value" :remove="handleImageClear">
241
+ <div class="o-search-image-input-preview-wrapper">
242
+ <OFigure
243
+ :src="image.previewUrl.value"
244
+ :preview="preview"
245
+ alt=""
246
+ class="o-search-image-input-preview-figure"
247
+ @preview="onPreviewChange"
248
+ />
249
+ <div class="o-search-image-input-preview-zoom">
250
+ <OIcon class="o-search-image-input-preview-zoom-icon"><IconImageZoomin /></OIcon>
251
+ </div>
252
+ <OIcon class="o-search-image-input-preview-remove" @click.stop="handleImageClear">
253
+ <IconImageClose />
254
+ </OIcon>
255
+ </div>
256
+ </slot>
257
+ </div>
258
+
259
+ <input
260
+ v-if="enableImageSearch"
261
+ ref="fileInputRef"
262
+ type="file"
263
+ :accept="acceptAttr"
264
+ class="o-search-image-input-file"
265
+ @change="handleFileSelect"
266
+ />
267
+ </div>
268
+ </template>
269
+
270
+ <style lang="scss" scoped>
271
+ .o-search-image-input {
272
+ display: flex;
273
+ flex-direction: column;
274
+ border: 1px solid transparent;
275
+ border-radius: var(--o-radius-xs);
276
+ background-color: var(--o-color-fill2);
277
+ transition: border-color var(--o-duration-m1);
278
+
279
+ &.is-expanded {
280
+ border-color: var(--o-color-primary1);
281
+
282
+ :deep(.o-input.el-input .el-input__wrapper),
283
+ :deep(.o_box-main) {
284
+ border: none !important;
285
+ border-radius: var(--o-radius-xs) var(--o-radius-xs) 0 0;
286
+ box-shadow: none !important;
287
+ }
288
+ }
289
+ }
290
+
291
+ .o-search-image-input-row {
292
+ display: flex;
293
+ align-items: center;
294
+ }
295
+
296
+ .o-search-image-input-input {
297
+ width: 100%;
298
+ }
299
+
300
+ .o-search-image-input-icon {
301
+ cursor: pointer;
302
+ color: var(--o-color-info1);
303
+ font-size: 16px;
304
+
305
+ &.close {
306
+ font-size: 16px;
307
+ @include x-svg-hover;
308
+ }
309
+
310
+ &.upload {
311
+ fill: var(--o-color-info1);
312
+ }
313
+ }
314
+
315
+ .o-search-image-input-upload-btn {
316
+ display: inline-flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ width: 16px;
320
+ height: 16px;
321
+ border-radius: 4px;
322
+ margin-right: 8px;
323
+ cursor: pointer;
324
+ flex-shrink: 0;
325
+
326
+ @include hover {
327
+ background-color: var(--o-color-control2-light);
328
+
329
+ .o-search-image-input-icon.upload {
330
+ color: var(--o-color-primary2);
331
+ }
332
+ }
333
+ }
334
+
335
+ .o-search-image-input-file {
336
+ display: none;
337
+ }
338
+
339
+ .o-search-image-input-thumb {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ margin-left: 8px;
343
+ position: relative;
344
+ overflow: visible;
345
+ flex-shrink: 0;
346
+
347
+ @include hover {
348
+ .o-search-image-input-thumb-remove,
349
+ .o-search-image-input-thumb-zoom {
350
+ opacity: 1;
351
+ }
352
+ }
353
+ }
354
+
355
+ .o-search-image-input-thumb-figure {
356
+ height: 40px;
357
+ width: 40px;
358
+ border-radius: 4px;
359
+ overflow: hidden;
360
+
361
+ :deep(img) {
362
+ height: 40px;
363
+ width: 40px;
364
+ object-fit: cover;
365
+ object-position: center;
366
+ border-radius: 4px;
367
+ }
368
+ }
369
+
370
+ .o-search-image-input-thumb-zoom {
371
+ position: absolute;
372
+ inset: 0;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ background-color: rgba(0, 0, 0, 0.3);
377
+ border-radius: 4px;
378
+ pointer-events: auto;
379
+ cursor: pointer;
380
+ opacity: 0;
381
+ transition: opacity var(--o-duration-m1);
382
+
383
+ .o-search-image-input-thumb-zoom-icon {
384
+ color: #fff;
385
+ font-size: 16px;
386
+ }
387
+ }
388
+
389
+ .o-search-image-input-thumb-remove {
390
+ position: absolute;
391
+ top: -8px;
392
+ right: -8px;
393
+ width: 16px;
394
+ height: 16px;
395
+ display: flex !important;
396
+ align-items: center;
397
+ justify-content: center;
398
+ cursor: pointer;
399
+ opacity: 0;
400
+ z-index: 1;
401
+ transition: opacity var(--o-duration-m1);
402
+
403
+ :deep(svg) {
404
+ width: 16px;
405
+ height: 16px;
406
+ fill: rgb(var(--o-mixedgray-9));
407
+ }
408
+ }
409
+
410
+ .o-search-image-input-preview {
411
+ padding: 0 12px 8px;
412
+
413
+ .o-search-image-input-preview-wrapper {
414
+ position: relative;
415
+ display: inline-flex;
416
+ overflow: visible;
417
+ margin-top: 8px;
418
+
419
+ @include hover {
420
+ .o-search-image-input-preview-remove,
421
+ .o-search-image-input-preview-zoom {
422
+ opacity: 1;
423
+ }
424
+ }
425
+ }
426
+
427
+ .o-search-image-input-preview-zoom {
428
+ position: absolute;
429
+ inset: 0;
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: center;
433
+ background-color: rgba(0, 0, 0, 0.3);
434
+ border-radius: 4px;
435
+ pointer-events: none;
436
+ opacity: 0;
437
+ transition: opacity var(--o-duration-m1);
438
+
439
+ .o-search-image-input-preview-zoom-icon {
440
+ color: #fff;
441
+ font-size: 24px;
442
+ }
443
+ }
444
+
445
+ .o-search-image-input-preview-figure {
446
+ height: 72px;
447
+ width: 72px;
448
+ border-radius: 4px;
449
+ overflow: hidden;
450
+
451
+ :deep(img) {
452
+ height: 72px;
453
+ width: 72px;
454
+ object-fit: cover;
455
+ object-position: center;
456
+ border-radius: 4px;
457
+ }
458
+ }
459
+
460
+ .o-search-image-input-preview-remove {
461
+ position: absolute;
462
+ top: -8px;
463
+ right: -8px;
464
+ width: 16px;
465
+ height: 16px;
466
+ display: flex !important;
467
+ align-items: center;
468
+ justify-content: center;
469
+ cursor: pointer;
470
+ opacity: 0;
471
+ z-index: 1;
472
+ transition: opacity var(--o-duration-m1);
473
+
474
+ :deep(svg) {
475
+ width: 16px;
476
+ height: 16px;
477
+ }
478
+ }
479
+ }
480
+ </style>
481
+
482
+ <style lang="scss">
483
+ .o-search-image-input-tooltip {
484
+ padding: var(--o-gap-3) var(--o-gap-4);
485
+ max-width: 240px;
486
+ @include tip2;
487
+ }
488
+ </style>