lau-ecom-design-system 1.0.23 → 1.0.26

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,432 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
3
+ import {
4
+ LauEcomUpcIconSearch,
5
+ LauEcomCoreIconNavClose as LauEcomUpcIconClose,
6
+ LauEcomUpcIconNavArrow,
7
+ } from "../LauEcomIcon";
8
+
9
+ interface Props {
10
+ // Input props
11
+ placeholder?: string;
12
+ isDisabled?: boolean;
13
+ modelValue?: string;
14
+ inputWidth?: string;
15
+ inputHeight?: string;
16
+ inputClass?: string;
17
+ inputBorderClass?: string;
18
+ inputBgClass?: string;
19
+ inputTextClass?: string;
20
+
21
+ // Botón de búsqueda props
22
+ searchButtonClass?: string;
23
+ searchButtonBgClass?: string;
24
+ searchButtonTextClass?: string;
25
+ searchIconSize?: {
26
+ width: string;
27
+ height: string;
28
+ };
29
+ searchIconClass?: string;
30
+
31
+ // Props para versión móvil
32
+ mobileButtonClass?: string;
33
+ mobileButtonBgClass?: string;
34
+ mobileButtonTextClass?: string;
35
+ mobileIconSize?: {
36
+ width: string;
37
+ height: string;
38
+ };
39
+
40
+ // Props para flecha de retorno
41
+ arrowButtonClass?: string;
42
+ arrowButtonBgClass?: string;
43
+ arrowIconClass?: string;
44
+ arrowIconSize?: {
45
+ width: string;
46
+ height: string;
47
+ };
48
+
49
+ // Props para panel expandido
50
+ expandedWidth?: string;
51
+ expandedBgClass?: string;
52
+ expandedShadowClass?: string;
53
+
54
+ // Props para overlay y panel móvil
55
+ overlayBgClass?: string;
56
+ overlayOpacityClass?: string;
57
+ mobilePanelBgClass?: string;
58
+ mobilePanelClass?: string;
59
+ }
60
+
61
+ const props = withDefaults(defineProps<Props>(), {
62
+ placeholder: "Quiero aprender...",
63
+ isDisabled: false,
64
+ modelValue: "",
65
+ inputWidth: "350px",
66
+ inputHeight: "40px",
67
+ inputClass: "dsEcom-rounded-lg",
68
+ inputBorderClass: "dsEcom-border dsEcom-border-neutral-80",
69
+ inputBgClass: "dsEcom-bg-white",
70
+ inputTextClass: "dsEcom-text-neutral-900",
71
+
72
+ searchButtonClass: "dsEcom-rounded-r-lg",
73
+ searchButtonBgClass: "bg-base-color-primary-20",
74
+ searchButtonTextClass: "text-base-color-primary-60",
75
+ searchIconSize: () => ({ width: "20", height: "20" }),
76
+ searchIconClass: "",
77
+
78
+ mobileButtonClass: "dsEcom-rounded-lg",
79
+ mobileButtonBgClass: "bg-base-color-primary-20",
80
+ mobileButtonTextClass: "text-base-color-primary-60",
81
+ mobileIconSize: () => ({ width: "24", height: "24" }),
82
+
83
+ arrowButtonClass: "dsEcom-rounded-lg",
84
+ arrowButtonBgClass: "bg-base-color-neutral-10",
85
+ arrowIconClass: "text-[#00A08C]",
86
+ arrowIconSize: () => ({ width: "24", height: "24" }),
87
+
88
+ expandedWidth: "800px",
89
+ expandedBgClass: "dsEcom-bg-white",
90
+ expandedShadowClass: "dsEcom-shadow-lg",
91
+
92
+ // Defaults para overlay y panel móvil
93
+ overlayBgClass: "dsEcom-bg-black",
94
+ overlayOpacityClass: "dsEcom-bg-opacity-50",
95
+ mobilePanelBgClass: "dsEcom-bg-white",
96
+ mobilePanelClass: "dsEcom-p-4"
97
+ });
98
+
99
+ const emit = defineEmits({
100
+ "update:modelValue": (value: string) => true,
101
+ "search": (value: string) => true,
102
+ "click:search-icon": () => true
103
+ });
104
+
105
+ const searchQuery = ref(props.modelValue);
106
+ const isExpanded = ref(false);
107
+ const showOverlay = ref(false);
108
+ const isMobileView = ref(false);
109
+ const isMobileSearchOpen = ref(false);
110
+
111
+ // Añadir ref para la posición expandida
112
+ const originalContainer = ref<HTMLElement | null>(null);
113
+ const expandedPosition = ref({ top: 0, left: 0 });
114
+
115
+ // Clases computadas
116
+ const inputClasses = computed(() => [
117
+ 'dsEcom-w-full dsEcom-transition-all dsEcom-duration-300',
118
+ props.inputClass,
119
+ props.inputBorderClass,
120
+ props.inputBgClass,
121
+ props.inputTextClass
122
+ ]);
123
+
124
+ const searchButtonClasses = computed(() => [
125
+ 'dsEcom-flex dsEcom-items-center dsEcom-px-3 dsEcom-transition-all dsEcom-duration-300',
126
+ props.searchButtonClass,
127
+ props.searchButtonBgClass,
128
+ props.searchButtonTextClass
129
+ ]);
130
+
131
+ const expandedContainerClasses = computed(() => [
132
+ 'dsEcom-fixed dsEcom-z-50 dsEcom-overflow-hidden dsEcom-transition-all dsEcom-duration-300',
133
+ props.expandedBgClass,
134
+ props.expandedShadowClass
135
+ ]);
136
+
137
+ // Manejo de posición del input expandido
138
+ const updateExpandedPosition = () => {
139
+ if (originalContainer.value) {
140
+ const rect = originalContainer.value.getBoundingClientRect();
141
+ expandedPosition.value = {
142
+ top: rect.top,
143
+ left: rect.left
144
+ };
145
+ }
146
+ };
147
+
148
+ // Handlers
149
+ const handleSearch = () => {
150
+ if (searchQuery.value?.trim()) {
151
+ emit("search", searchQuery.value);
152
+ emit("update:modelValue", searchQuery.value);
153
+ emit("click:search-icon");
154
+ if (isMobileView.value) {
155
+ handleMobileSearchClose();
156
+ } else {
157
+ closeSearch();
158
+ }
159
+ }
160
+ };
161
+
162
+ const handleInput = () => {
163
+ emit("update:modelValue", searchQuery.value);
164
+ };
165
+
166
+ // Modificar handleFocus
167
+ const handleFocus = () => {
168
+ if (!isMobileView.value) {
169
+ updateExpandedPosition();
170
+ showOverlay.value = true;
171
+ isExpanded.value = true;
172
+ }
173
+ };
174
+
175
+ // Modificar closeSearch
176
+ const closeSearch = () => {
177
+ showOverlay.value = false;
178
+ isExpanded.value = false;
179
+ };
180
+
181
+ const handleMobileSearchClick = () => {
182
+ isMobileSearchOpen.value = true;
183
+ };
184
+
185
+ const handleMobileSearchClose = () => {
186
+ const panel = document.querySelector('.mobile-search-panel') as HTMLElement;
187
+ if (panel) {
188
+ panel.style.transform = 'translateY(100%)';
189
+ panel.style.opacity = '0';
190
+ }
191
+
192
+ setTimeout(() => {
193
+ isMobileSearchOpen.value = false;
194
+ searchQuery.value = '';
195
+ emit('update:modelValue', '');
196
+ }, 300);
197
+ };
198
+
199
+ // Lifecycle hooks
200
+ onMounted(() => {
201
+ checkMobileView();
202
+ window.addEventListener('resize', checkMobileView);
203
+ window.addEventListener('resize', updateExpandedPosition);
204
+ });
205
+
206
+ onBeforeUnmount(() => {
207
+ window.removeEventListener('resize', checkMobileView);
208
+ window.removeEventListener('resize', updateExpandedPosition);
209
+ });
210
+
211
+ const checkMobileView = () => {
212
+ isMobileView.value = window.innerWidth < 768;
213
+ };
214
+
215
+ watch(() => props.modelValue, (newValue) => {
216
+ if (newValue !== searchQuery.value) {
217
+ searchQuery.value = newValue;
218
+ }
219
+ });
220
+ </script>
221
+
222
+ <template>
223
+ <div class="dsEcom-relative">
224
+ <!-- Versión Desktop -->
225
+ <div v-show="!isMobileView" class="dsEcom-transform-gpu">
226
+ <div
227
+ ref="originalContainer"
228
+ :class="[
229
+ 'dsEcom-inline-flex dsEcom-items-center dsEcom-transition-all dsEcom-duration-300',
230
+ isExpanded ? [
231
+ 'dsEcom-fixed dsEcom-z-[60]',
232
+ expandedBgClass,
233
+ expandedShadowClass
234
+ ] : ''
235
+ ]"
236
+ :style="{
237
+ width: isExpanded ? props.expandedWidth : inputWidth,
238
+ maxWidth: isExpanded ? '90vw' : 'none',
239
+ top: isExpanded ? `${expandedPosition.top}px` : 'auto',
240
+ left: isExpanded ? `${expandedPosition.left}px` : 'auto'
241
+ }"
242
+ >
243
+ <div class="dsEcom-relative dsEcom-w-full">
244
+ <div class="dsEcom-flex dsEcom-items-center">
245
+ <input
246
+ v-model="searchQuery"
247
+ type="text"
248
+ :placeholder="placeholder"
249
+ :disabled="isDisabled"
250
+ :style="{ height: inputHeight }"
251
+ :class="[inputClasses, 'dsEcom-w-full dsEcom-pr-12']"
252
+ @focus="handleFocus"
253
+ @input="handleInput"
254
+ @keyup.enter="handleSearch"
255
+ />
256
+ <button
257
+ @click="handleSearch"
258
+ :class="[
259
+ searchButtonClasses,
260
+ 'dsEcom-absolute dsEcom-right-0 dsEcom-top-1/2 dsEcom-transform dsEcom--translate-y-1/2 dsEcom-px-3'
261
+ ]"
262
+ >
263
+ <LauEcomUpcIconSearch
264
+ :width="searchIconSize.width"
265
+ :height="searchIconSize.height"
266
+ :class="searchIconClass"
267
+ />
268
+ </button>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+
274
+ <!-- Versión Mobile -->
275
+ <div v-show="isMobileView" class="dsEcom-transform-gpu">
276
+ <!-- Botón de búsqueda móvil -->
277
+ <button
278
+ v-show="!isMobileSearchOpen"
279
+ @click="handleMobileSearchClick"
280
+ :class="[
281
+ 'dsEcom-p-2 dsEcom-rounded-lg dsEcom-transition-all dsEcom-duration-300',
282
+ mobileButtonClass,
283
+ mobileButtonBgClass,
284
+ mobileButtonTextClass
285
+ ]"
286
+ >
287
+ <LauEcomUpcIconSearch
288
+ :width="mobileIconSize.width"
289
+ :height="mobileIconSize.height"
290
+ />
291
+ </button>
292
+
293
+ <!-- Panel de búsqueda móvil -->
294
+ <div
295
+ v-show="isMobileSearchOpen"
296
+ :class="[
297
+ 'mobile-search-panel dsEcom-fixed dsEcom-inset-0 dsEcom-z-50',
298
+ mobilePanelBgClass,
299
+ mobilePanelClass
300
+ ]"
301
+ >
302
+ <div class="dsEcom-flex dsEcom-items-center dsEcom-gap-2 dsEcom-p-4">
303
+ <button
304
+ @click="handleMobileSearchClose"
305
+ :class="[
306
+ 'dsEcom-p-2 dsEcom-rounded-lg dsEcom-transition-all dsEcom-duration-300',
307
+ arrowButtonClass,
308
+ arrowButtonBgClass
309
+ ]"
310
+ >
311
+ <LauEcomUpcIconNavArrow
312
+ :width="arrowIconSize.width"
313
+ :height="arrowIconSize.height"
314
+ class="dsEcom-transform dsEcom-rotate-90"
315
+ :class="arrowIconClass"
316
+ />
317
+ </button>
318
+
319
+ <div class="dsEcom-flex-1">
320
+ <div class="dsEcom-relative">
321
+ <input
322
+ v-model="searchQuery"
323
+ type="text"
324
+ :placeholder="placeholder"
325
+ :disabled="isDisabled"
326
+ :style="{ height: inputHeight }"
327
+ :class="[
328
+ inputClasses,
329
+ 'dsEcom-w-full dsEcom-pr-12 dsEcom-pl-4',
330
+ 'dsEcom-border dsEcom-border-neutral-200 dsEcom-rounded-lg'
331
+ ]"
332
+ @input="handleInput"
333
+ @keyup.enter="handleSearch"
334
+ autofocus
335
+ />
336
+ <button
337
+ @click="handleSearch"
338
+ :class="[
339
+ searchButtonClasses,
340
+ 'dsEcom-absolute dsEcom-right-0 dsEcom-top-1/2 dsEcom-transform dsEcom--translate-y-1/2 dsEcom-px-3'
341
+ ]"
342
+ >
343
+ <LauEcomUpcIconSearch
344
+ :width="searchIconSize.width"
345
+ :height="searchIconSize.height"
346
+ :class="searchIconClass"
347
+ />
348
+ </button>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <!-- Overlay (fuera del componente principal) -->
357
+ <Teleport to="body">
358
+ <Transition name="fade">
359
+ <div
360
+ v-if="showOverlay"
361
+ :class="[
362
+ 'dsEcom-fixed dsEcom-inset-0 dsEcom-z-50 dsEcom-transition-opacity dsEcom-duration-300',
363
+ overlayBgClass,
364
+ overlayOpacityClass
365
+ ]"
366
+ @click="closeSearch"
367
+ ></div>
368
+ </Transition>
369
+ </Teleport>
370
+ </template>
371
+
372
+ <style scoped>
373
+ .mobile-search-panel {
374
+ transform-origin: top center;
375
+ will-change: transform, opacity;
376
+ animation: slideIn 0.3s ease-out;
377
+ }
378
+
379
+ @keyframes slideIn {
380
+ from {
381
+ transform: translateY(-100%);
382
+ opacity: 0;
383
+ }
384
+ to {
385
+ transform: translateY(0);
386
+ opacity: 1;
387
+ }
388
+ }
389
+
390
+ .dsEcom-transform-gpu {
391
+ transform: translateZ(0);
392
+ backface-visibility: hidden;
393
+ perspective: 1000px;
394
+ will-change: transform, opacity;
395
+ }
396
+
397
+ /* Asegurarse de que el overlay cubra toda la pantalla */
398
+ .dsEcom-fixed.dsEcom-inset-0 {
399
+ position: fixed;
400
+ top: 0;
401
+ right: 0;
402
+ bottom: 0;
403
+ left: 0;
404
+ width: 100vw;
405
+ height: 100vh;
406
+ }
407
+
408
+ /* Ajustar z-index para asegurar que el overlay esté por debajo del input expandido */
409
+ .dsEcom-z-50 {
410
+ z-index: 50;
411
+ }
412
+
413
+ .dsEcom-z-\[60\] {
414
+ z-index: 60;
415
+ }
416
+
417
+ /* Transiciones suaves */
418
+ .dsEcom-transition-all {
419
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
420
+ }
421
+
422
+ /* Transición para el overlay */
423
+ .fade-enter-active,
424
+ .fade-leave-active {
425
+ transition: opacity 0.3s ease;
426
+ }
427
+
428
+ .fade-enter-from,
429
+ .fade-leave-to {
430
+ opacity: 0;
431
+ }
432
+ </style>