lau-ecom-design-system 1.0.26 → 1.0.27

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.
@@ -1,446 +1,154 @@
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
- placeholder?: string;
11
- isDisabled?: boolean;
12
- modelValue?: string;
13
- forceClose?: boolean;
14
- buttonColorClass?: string;
15
- buttonTextColorClass?: string;
16
- buttonClass?: string;
17
- inputClass?: string;
18
- containerClass?: string;
19
- expandedWidth?: string;
20
- isMobileSearch?: boolean;
21
- arrowColorClass?: string;
22
- arrowButtonClass?: string;
23
- mobileInputWidth?: string;
24
- mobileInputHeight?: string;
25
- mobilePanelClass?: string;
26
- expandedBackgroundClass?: string;
27
- overlayClass?: string;
28
- }
29
-
30
- const props = withDefaults(defineProps<Props>(), {
31
- placeholder: "Quiero aprender...",
32
- isDisabled: false,
33
- modelValue: "",
34
- forceClose: false,
35
- buttonColorClass: "bg-base-color-primary-20",
36
- buttonTextColorClass: "text-base-color-primary-60",
37
- buttonClass: "",
38
- inputClass: "dsEcom-h-10 dsEcom-rounded-lg",
39
- containerClass: "",
40
- expandedWidth: "50vw",
41
- isMobileSearch: false,
42
- arrowColorClass: "text-neutral-100",
43
- arrowButtonClass: "",
44
- mobileInputWidth: "240px",
45
- mobileInputHeight: "40px",
46
- mobilePanelClass: "bg-white",
47
- expandedBackgroundClass: "dsEcom-bg-white",
48
- overlayClass: "dsEcom-bg-black dsEcom-opacity-50"
49
- });
50
-
51
- const emit = defineEmits({
52
- "update:modelValue": (value: string) => true,
53
- "search": (value: string) => true,
54
- "click:search-icon": () => true
55
- });
56
-
57
- const searchQuery = ref(props.modelValue);
58
- const isExpanded = ref(false);
59
- const showOverlay = ref(false);
60
-
61
- const isMobileView = ref(false);
62
- const isMobileSearchOpen = ref(false);
63
-
64
- const handleSearch = () => {
65
- if (searchQuery.value && searchQuery.value.trim()) {
66
- emit("search", searchQuery.value);
67
- emit("update:modelValue", searchQuery.value);
68
- emit("click:search-icon");
69
- closeSearch();
70
- }
71
- };
72
-
73
- const handleInput = () => {
74
- emit("update:modelValue", searchQuery.value);
75
- };
76
-
77
- const clearSearch = () => {
78
- searchQuery.value = "";
79
- emit("update:modelValue", "");
80
- };
81
-
82
- const closeSearch = () => {
83
- showOverlay.value = false;
84
- setTimeout(() => {
85
- isExpanded.value = false;
86
- }, 300);
87
- };
88
-
89
- const handleFocus = () => {
90
- isExpanded.value = true;
91
- showOverlay.value = true;
92
- };
93
-
94
- const containerClasses = computed(() => {
95
- return [
96
- "dsEcom-transition-transform dsEcom-duration-300 dsEcom-ease-in-out dsEcom-relative",
97
- {
98
- "dsEcom-w-[250px] md:dsEcom-w-[350px]": !isExpanded.value
99
- }
100
- ];
101
- });
102
-
103
- const overlayClasses = computed(() => [
104
- "dsEcom-fixed dsEcom-inset-0 dsEcom-transition-opacity dsEcom-duration-300 dsEcom-ease-in-out dsEcom-z-40",
105
- props.overlayClass,
106
- {
107
- "dsEcom-opacity-100": showOverlay.value,
108
- "dsEcom-opacity-0 dsEcom-pointer-events-none": !showOverlay.value
109
- }
110
- ]);
111
-
112
- const originalContainer = ref<HTMLElement | null>(null);
113
- const expandedContainer = ref<HTMLElement | null>(null);
114
-
115
- const updateExpandedPosition = () => {
116
- if (originalContainer.value && expandedContainer.value) {
117
- const rect = originalContainer.value.getBoundingClientRect();
118
- const viewportHeight = window.innerHeight;
119
- const viewportWidth = window.innerWidth;
120
-
121
- let topPosition;
122
- if (rect.top < 0) {
123
- topPosition = '20px';
124
- } else if (rect.top > viewportHeight) {
125
- topPosition = '20px';
126
- } else {
127
- topPosition = `${rect.top}px`;
128
- }
129
-
130
- // Mantenemos la posición original del input
131
- const leftPosition = `${rect.left}px`;
132
-
133
- expandedContainer.value.style.position = 'fixed';
134
- expandedContainer.value.style.top = topPosition;
135
- expandedContainer.value.style.left = leftPosition;
136
- expandedContainer.value.style.transform = 'none';
137
- expandedContainer.value.style.width = props.expandedWidth;
138
- expandedContainer.value.style.maxWidth = '90vw';
139
-
140
- // Ajustamos si se sale por la derecha
141
- const expandedWidth = parseInt(props.expandedWidth);
142
- if (rect.left + expandedWidth > viewportWidth) {
143
- // Si se sale por la derecha, ajustamos el ancho para que quepa
144
- const availableWidth = viewportWidth - rect.left - 32; // 32px de margen
145
- expandedContainer.value.style.width = `${availableWidth}px`;
146
- }
147
- }
148
- };
149
-
150
- const checkMobileView = () => {
151
- isMobileView.value = window.innerWidth < 768;
152
- };
153
-
154
- const handleMobileSearchClick = () => {
155
- isMobileSearchOpen.value = true;
156
- nextTick(() => {
157
- if (expandedContainer.value) {
158
- const input = expandedContainer.value.querySelector('input');
159
- input?.focus();
160
- }
161
- });
162
- };
163
-
164
- const handleMobileSearchClose = () => {
165
- // Primero ocultamos con la transición
166
- const panel = document.querySelector('.dsEcom-fixed') as HTMLElement;
167
- if (panel) {
168
- panel.style.opacity = '0';
169
- panel.style.transform = 'translateY(100%)';
170
- }
171
-
172
- // Luego actualizamos el estado después de la transición
173
- setTimeout(() => {
174
- isMobileSearchOpen.value = false;
175
- searchQuery.value = '';
176
- emit('update:modelValue', '');
177
- }, 300);
178
- };
179
-
180
- const handleMobileSearch = () => {
181
- if (searchQuery.value && searchQuery.value.trim()) {
182
- emit("search", searchQuery.value);
183
- emit("update:modelValue", searchQuery.value);
184
- emit("click:search-icon");
185
- handleMobileSearchClose();
186
- }
187
- };
188
-
189
- const handleMobileClear = () => {
190
- clearSearch();
191
- };
192
-
193
- onMounted(() => {
194
- checkMobileView();
195
- window.addEventListener('resize', updateExpandedPosition);
196
- window.addEventListener('resize', checkMobileView);
197
- });
198
-
199
- onBeforeUnmount(() => {
200
- window.removeEventListener('resize', updateExpandedPosition);
201
- window.removeEventListener('resize', checkMobileView);
202
- });
203
-
204
- watch(isExpanded, (newValue) => {
205
- if (newValue) {
206
- nextTick(updateExpandedPosition);
207
- }
208
- });
209
-
210
- watch(() => props.forceClose, (newValue) => {
211
- if (newValue) {
212
- closeSearch();
213
- }
214
- });
215
-
216
- watch(() => props.modelValue, (newValue) => {
217
- if (newValue !== searchQuery.value) {
218
- searchQuery.value = newValue;
219
- }
220
- });
221
- </script>
222
-
223
- <template>
224
- <div class="dsEcom-relative">
225
- <!-- Versión móvil - Solo botón de búsqueda -->
226
- <button
227
- v-show="isMobileView && !isMobileSearchOpen"
228
- @click="handleMobileSearchClick"
229
- class="dsEcom-p-2 dsEcom-rounded-lg dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu"
230
- :class="[buttonColorClass]"
231
- >
232
- <LauEcomUpcIconSearch
233
- width="24"
234
- height="24"
235
- :class="[buttonTextColorClass]"
236
- />
237
- </button>
238
-
239
- <!-- Capa blanca móvil -->
240
- <div
241
- v-show="isMobileView && isMobileSearchOpen"
242
- class="dsEcom-fixed dsEcom-inset-0 dsEcom-z-50 dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu"
243
- :class="[mobilePanelClass]"
244
- :style="{
245
- opacity: isMobileSearchOpen ? '1' : '0',
246
- transform: isMobileSearchOpen ? 'translateY(0)' : 'translateY(100%)'
247
- }"
248
- >
249
- <div class="dsEcom-flex dsEcom-items-center dsEcom-gap-2 dsEcom-p-4">
250
- <!-- Input y botones -->
251
- <div class="dsEcom-flex dsEcom-items-center dsEcom-gap-2 dsEcom-flex-1 dsEcom-transform-gpu">
252
- <button
253
- @click="handleMobileSearchClose"
254
- class="dsEcom-p-2 dsEcom-rounded-lg dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu"
255
- :class="[arrowButtonClass]"
256
- >
257
- <LauEcomUpcIconNavArrow
258
- width="24"
259
- height="24"
260
- class="dsEcom-transform dsEcom-rotate-90 dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu"
261
- :class="[arrowColorClass]"
262
- color="currentColor"
263
- />
264
- </button>
265
-
266
- <div class="dsEcom-flex dsEcom-items-center dsEcom-gap-0 dsEcom-flex-1 dsEcom-transform-gpu">
267
- <input
268
- v-model="searchQuery"
269
- type="text"
270
- :placeholder="placeholder"
271
- :disabled="isDisabled"
272
- :style="{
273
- width: props.mobileInputWidth,
274
- height: props.mobileInputHeight
275
- }"
276
- class="lau-ecom-input dsEcom-pl-4 dsEcom-pr-4 dsEcom-border dsEcom-border-neutral-80 dsEcom-rounded-l-lg dsEcom-focus:outline-none dsEcom-focus:ring-2 dsEcom-focus:ring-primary-60 dsEcom-flex-1 dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu"
277
- @input="handleInput"
278
- @keyup.enter="handleMobileSearch"
279
- />
280
- <button
281
- @click="handleMobileSearch"
282
- :style="{ height: props.mobileInputHeight }"
283
- :class="[
284
- 'dsEcom-px-3 dsEcom-rounded-r-lg dsEcom-border-0 dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu',
285
- buttonColorClass,
286
- buttonTextColorClass
287
- ]"
288
- >
289
- <LauEcomUpcIconSearch width="20" height="20" color="currentColor" />
290
- </button>
291
- </div>
292
-
293
- <button
294
- v-show="searchQuery.length >= 3"
295
- @click="handleMobileClear"
296
- class="dsEcom-p-2 dsEcom-text-neutral-100 hover:dsEcom-text-neutral-80 dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu"
297
- >
298
- <LauEcomUpcIconClose width="16" height="16" />
299
- </button>
300
- </div>
301
- </div>
302
- </div>
303
-
304
- <!-- Versión desktop -->
305
- <div v-show="!isMobileView" class="dsEcom-transition-all dsEcom-duration-300 dsEcom-transform-gpu">
306
- <div
307
- :class="[containerClasses, props.containerClass]"
308
- ref="originalContainer"
309
- >
310
- <div class="dsEcom-relative" :class="{ 'dsEcom-invisible': isExpanded }">
311
- <input
312
- v-model="searchQuery"
313
- type="text"
314
- :placeholder="placeholder"
315
- :disabled="isDisabled"
316
- :class="[
317
- 'lau-ecom-input dsEcom-border dsEcom-border-neutral-80 dsEcom-pl-4 dsEcom-pr-24 dsEcom-w-full dsEcom-focus:outline-none dsEcom-focus:ring-2 dsEcom-focus:ring-primary-60 dsEcom-transition-all dsEcom-duration-300',
318
- props.inputClass,
319
- { 'dsEcom-opacity-0': isExpanded }
320
- ]"
321
- @focus="handleFocus"
322
- @input="handleInput"
323
- @keyup.enter="handleSearch"
324
- />
325
- <div class="dsEcom-absolute dsEcom-right-0 dsEcom-inset-y-0 dsEcom-flex dsEcom-items-stretch">
326
- <button
327
- v-if="searchQuery.length >= 3"
328
- @click="clearSearch"
329
- :class="[
330
- 'dsEcom-flex dsEcom-items-center dsEcom-px-1.5 dsEcom-text-neutral-100 hover:dsEcom-text-neutral-80 dsEcom-transition-all dsEcom-duration-300',
331
- props.inputClass
332
- ]"
333
- >
334
- <LauEcomUpcIconClose width="16" height="16" />
335
- </button>
336
-
337
- <button
338
- @click="handleSearch"
339
- :class="[
340
- 'dsEcom-flex dsEcom-items-center dsEcom-px-3 dsEcom-transition-all dsEcom-duration-300',
341
- props.inputClass?.includes('rounded') ? props.inputClass : 'dsEcom-rounded-r-lg',
342
- props.buttonColorClass,
343
- props.buttonTextColorClass,
344
- 'dsEcom-border-0'
345
- ]"
346
- style="margin: 1px; height: calc(100% - 2px);"
347
- >
348
- <LauEcomUpcIconSearch width="20" height="20" color="currentColor" />
349
- </button>
350
- </div>
351
- </div>
352
- </div>
353
-
354
- <!-- Overlay -->
355
- <div
356
- v-show="isExpanded"
357
- :class="[
358
- overlayClasses,
359
- 'dsEcom-transition-opacity dsEcom-duration-300 dsEcom-ease-in-out'
360
- ]"
361
- @click="closeSearch"
362
- ></div>
363
-
364
- <!-- Versión expandida -->
365
- <div
366
- v-show="isExpanded"
367
- class="dsEcom-fixed dsEcom-z-50 dsEcom-shadow-lg dsEcom-overflow-hidden dsEcom-transition-all dsEcom-duration-300"
368
- :class="[
369
- props.inputClass?.includes('rounded') ? props.inputClass : 'dsEcom-rounded-lg',
370
- expandedBackgroundClass
371
- ]"
372
- ref="expandedContainer"
373
- >
374
- <div class="dsEcom-relative">
375
- <input
376
- v-model="searchQuery"
377
- type="text"
378
- :placeholder="placeholder"
379
- :disabled="isDisabled"
380
- :class="[
381
- 'lau-ecom-input dsEcom-border dsEcom-border-neutral-80 dsEcom-pl-4 dsEcom-pr-24 dsEcom-w-full dsEcom-focus:outline-none dsEcom-focus:ring-2 dsEcom-focus:ring-primary-60 dsEcom-transition-all dsEcom-duration-300',
382
- props.inputClass
383
- ]"
384
- @input="handleInput"
385
- @keyup.enter="handleSearch"
386
- autofocus
387
- />
388
- <div class="dsEcom-absolute dsEcom-right-0 dsEcom-inset-y-0 dsEcom-flex dsEcom-items-stretch">
389
- <button
390
- v-if="searchQuery.length >= 3"
391
- @click="clearSearch"
392
- :class="[
393
- 'dsEcom-flex dsEcom-items-center dsEcom-px-1.5 dsEcom-text-neutral-100 hover:dsEcom-text-neutral-80 dsEcom-transition-all dsEcom-duration-300',
394
- props.inputClass
395
- ]"
396
- >
397
- <LauEcomUpcIconClose width="16" height="16" />
398
- </button>
399
-
400
- <button
401
- @click="handleSearch"
402
- :class="[
403
- 'dsEcom-flex dsEcom-items-center dsEcom-px-3 dsEcom-transition-all dsEcom-duration-300',
404
- { 'dsEcom-rounded-r-lg': !props.inputClass?.includes('rounded') },
405
- props.buttonColorClass,
406
- props.buttonTextColorClass,
407
- 'dsEcom-border-0'
408
- ]"
409
- style="margin: 1px; height: calc(100% - 2px);"
410
- >
411
- <LauEcomUpcIconSearch width="20" height="20" color="currentColor" />
412
- </button>
413
- </div>
414
- </div>
415
- </div>
416
- </div>
417
- </div>
418
- </template>
419
-
420
- <style scoped>
421
- .lau-ecom-input {
422
- transition: all 0.3s ease-in-out;
423
- backface-visibility: hidden;
424
- transform: translateZ(0);
425
- -webkit-font-smoothing: antialiased;
426
- -moz-osx-font-smoothing: grayscale;
427
- will-change: transform, opacity;
428
- }
429
-
430
- .dsEcom-transform-gpu {
431
- transform: translateZ(0);
432
- backface-visibility: hidden;
433
- perspective: 1000px;
434
- will-change: transform, opacity;
435
- }
436
-
437
- /* Agregamos transiciones específicas para móvil */
438
- @media (max-width: 768px) {
439
- .dsEcom-fixed {
440
- transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
441
- will-change: transform, opacity;
442
- }
443
- }
444
- </style>
445
-
446
-
1
+ <script lang="ts" setup>
2
+ import { ref, computed } from 'vue';
3
+ import { LauEcomUpcIconSearch, LauEcomUpcIconClose } from '../LauEcomIcon';
4
+
5
+ interface Props {
6
+ modelValue?: string;
7
+ placeholder?: string;
8
+ width?: string;
9
+ inputClass?: string;
10
+ inputBorderClass?: string;
11
+ inputBgClass?: string;
12
+ inputTextClass?: string;
13
+ inputFocusClass?: string;
14
+
15
+ // Props para el icono de búsqueda
16
+ searchIconClass?: string;
17
+ searchIconBgClass?: string;
18
+ searchIconSize?: {
19
+ width: string;
20
+ height: string;
21
+ };
22
+
23
+ // Props para el icono de limpiar
24
+ clearIconClass?: string;
25
+ clearIconBgClass?: string;
26
+ clearIconSize?: {
27
+ width: string;
28
+ height: string;
29
+ };
30
+ }
31
+
32
+ const props = withDefaults(defineProps<Props>(), {
33
+ modelValue: '',
34
+ placeholder: 'Buscar...',
35
+ width: '100%',
36
+ inputClass: 'dsEcom-rounded-lg',
37
+ inputBorderClass: 'dsEcom-border dsEcom-border-neutral-80',
38
+ inputBgClass: 'dsEcom-bg-white',
39
+ inputTextClass: 'dsEcom-text-neutral-900',
40
+ inputFocusClass: 'focus:dsEcom-ring-1 focus:dsEcom-ring-primary-60 focus:dsEcom-border-primary-60',
41
+
42
+ searchIconClass: 'dsEcom-text-neutral-60',
43
+ searchIconBgClass: 'dsEcom-bg-transparent',
44
+ searchIconSize: () => ({ width: '20', height: '20' }),
45
+
46
+ clearIconClass: 'dsEcom-text-neutral-60',
47
+ clearIconBgClass: 'dsEcom-bg-transparent',
48
+ clearIconSize: () => ({ width: '20', height: '20' })
49
+ });
50
+
51
+ const emit = defineEmits<{
52
+ (e: 'update:modelValue', value: string): void;
53
+ (e: 'search', value: string): void;
54
+ (e: 'clear'): void;
55
+ (e: 'focus', event: FocusEvent): void;
56
+ (e: 'blur', event: FocusEvent): void;
57
+ }>();
58
+
59
+ const inputValue = ref(props.modelValue);
60
+
61
+ const inputClasses = computed(() => [
62
+ 'dsEcom-w-full',
63
+ 'dsEcom-py-2 dsEcom-pl-4 dsEcom-pr-12',
64
+ 'dsEcom-transition-all dsEcom-duration-300',
65
+ props.inputClass,
66
+ props.inputBorderClass,
67
+ props.inputBgClass,
68
+ props.inputTextClass,
69
+ 'focus:dsEcom-outline-none',
70
+ props.inputFocusClass
71
+ ]);
72
+
73
+ const handleInput = (event: Event) => {
74
+ const value = (event.target as HTMLInputElement).value;
75
+ inputValue.value = value;
76
+ emit('update:modelValue', value);
77
+ };
78
+
79
+ const handleClear = () => {
80
+ inputValue.value = '';
81
+ emit('update:modelValue', '');
82
+ emit('clear');
83
+ };
84
+
85
+ const handleKeyUp = (event: KeyboardEvent) => {
86
+ if (event.key === 'Enter' && inputValue.value) {
87
+ emit('search', inputValue.value);
88
+ }
89
+ };
90
+ </script>
91
+
92
+ <template>
93
+ <div
94
+ class="dsEcom-relative dsEcom-inline-block"
95
+ :style="{ width }"
96
+ >
97
+ <input
98
+ type="text"
99
+ v-model="inputValue"
100
+ :placeholder="placeholder"
101
+ :class="inputClasses"
102
+ @input="handleInput"
103
+ @keyup="handleKeyUp"
104
+ @focus="(e) => emit('focus', e)"
105
+ @blur="(e) => emit('blur', e)"
106
+ />
107
+
108
+ <!-- Botón de limpiar -->
109
+ <button
110
+ v-if="inputValue"
111
+ type="button"
112
+ @click="handleClear"
113
+ :class="[
114
+ 'dsEcom-absolute dsEcom-right-8 dsEcom-top-1/2 dsEcom-transform dsEcom--translate-y-1/2',
115
+ 'dsEcom-p-1 dsEcom-rounded-full dsEcom-transition-colors dsEcom-duration-300',
116
+ clearIconBgClass,
117
+ 'hover:dsEcom-opacity-80'
118
+ ]"
119
+ >
120
+ <LauEcomUpcIconClose
121
+ :width="clearIconSize.width"
122
+ :height="clearIconSize.height"
123
+ :class="[clearIconClass, 'dsEcom-transition-colors dsEcom-duration-300']"
124
+ />
125
+ </button>
126
+
127
+ <!-- Icono de búsqueda -->
128
+ <button
129
+ type="button"
130
+ :class="[
131
+ 'dsEcom-absolute dsEcom-right-0 dsEcom-top-0 dsEcom-bottom-0',
132
+ 'dsEcom-flex dsEcom-items-center dsEcom-justify-center',
133
+ 'dsEcom-px-4',
134
+ 'dsEcom-transition-colors dsEcom-duration-300',
135
+ 'dsEcom-rounded-r-lg',
136
+ searchIconBgClass,
137
+ 'hover:dsEcom-opacity-80'
138
+ ]"
139
+ @click="$emit('search', inputValue)"
140
+ >
141
+ <LauEcomUpcIconSearch
142
+ :width="searchIconSize.width"
143
+ :height="searchIconSize.height"
144
+ :class="[searchIconClass, 'dsEcom-transition-colors dsEcom-duration-300']"
145
+ />
146
+ </button>
147
+ </div>
148
+ </template>
149
+
150
+ <style scoped>
151
+ .dsEcom-transform {
152
+ transform: translateY(-50%);
153
+ }
154
+ </style>