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,324 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, nextTick, watch, onMounted } from "vue";
3
+ import { LauEcomUpcIconSearch } from "../LauEcomIcon";
4
+
5
+ interface Props {
6
+ placeholder?: string;
7
+ isDisabled?: boolean;
8
+ modelValue?: string;
9
+ inputWidth?: string;
10
+ inputHeight?: string;
11
+ inputClass?: string;
12
+ inputBorderClass?: string;
13
+ inputBgClass?: string;
14
+ inputTextClass?: string;
15
+ searchButtonClass?: string;
16
+ searchButtonBgClass?: string;
17
+ searchButtonTextClass?: string;
18
+ searchIconSize?: {
19
+ width: string;
20
+ height: string;
21
+ };
22
+ searchIconClass?: string;
23
+ expandedWidth?: string;
24
+ expandedBgClass?: string;
25
+ expandedShadowClass?: string;
26
+ overlayBgClass?: string;
27
+ overlayOpacityClass?: string;
28
+ isMobile?: boolean;
29
+ mobileButtonBgClass?: string;
30
+ mobileButtonTextClass?: string;
31
+ searchButtonId?: string;
32
+ shouldClose?: boolean;
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ placeholder: "Buscar...",
37
+ isDisabled: false,
38
+ modelValue: "",
39
+ inputWidth: "350px",
40
+ inputHeight: "40px",
41
+ inputClass: "dsEcom-rounded-lg",
42
+ inputBorderClass: "dsEcom-border dsEcom-border-neutral-80",
43
+ inputBgClass: "dsEcom-bg-white",
44
+ inputTextClass: "dsEcom-text-neutral-900",
45
+ searchButtonClass: "dsEcom-rounded-r-lg",
46
+ searchButtonBgClass: "bg-base-color-primary-20",
47
+ searchButtonTextClass: "text-base-color-primary-60",
48
+ searchIconSize: () => ({ width: "20", height: "20" }),
49
+ searchIconClass: "",
50
+ expandedWidth: "800px",
51
+ expandedBgClass: "dsEcom-bg-white",
52
+ expandedShadowClass: "dsEcom-shadow-lg",
53
+ overlayBgClass: "dsEcom-bg-black",
54
+ overlayOpacityClass: "dsEcom-bg-opacity-50",
55
+ isMobile: false,
56
+ mobileButtonBgClass: "dsEcom-bg-primary-60",
57
+ mobileButtonTextClass: "dsEcom-text-white",
58
+ searchButtonId: "",
59
+ shouldClose: false
60
+ });
61
+
62
+ const emit = defineEmits({
63
+ "update:modelValue": (value: string) => true,
64
+ "search": (value: string) => true,
65
+ "click:search-icon": () => true,
66
+ "clear": () => true,
67
+ "update:should-close": (value: boolean) => true
68
+ });
69
+
70
+ const searchQuery = ref(props.modelValue);
71
+ const isExpanded = ref(false);
72
+ const isMounted = ref(false);
73
+ const isTransitioning = ref(false);
74
+ const originalContainer = ref<HTMLElement | null>(null);
75
+
76
+ const inputClasses = computed(() => [
77
+ 'dsEcom-w-full dsEcom-transition-all dsEcom-duration-300',
78
+ props.inputClass,
79
+ props.inputBorderClass,
80
+ props.inputBgClass,
81
+ props.inputTextClass
82
+ ]);
83
+
84
+ const searchButtonClasses = computed(() => [
85
+ 'dsEcom-flex dsEcom-items-center dsEcom-h-full',
86
+ props.searchButtonClass,
87
+ props.searchButtonBgClass,
88
+ props.searchButtonTextClass
89
+ ]);
90
+
91
+ const showClearButton = computed(() => {
92
+ return searchQuery.value && searchQuery.value.length >= 3;
93
+ });
94
+
95
+ const handleSearch = () => {
96
+ if (searchQuery.value?.trim()) {
97
+ emit("search", searchQuery.value);
98
+ emit("update:modelValue", searchQuery.value);
99
+ emit("click:search-icon");
100
+ closeSearch();
101
+ }
102
+ };
103
+
104
+ const handleInput = () => {
105
+ emit("update:modelValue", searchQuery.value || '');
106
+ };
107
+
108
+ const handleFocus = () => {
109
+ isTransitioning.value = true;
110
+ isExpanded.value = true;
111
+ };
112
+
113
+ const closeSearch = () => {
114
+ isTransitioning.value = true;
115
+ isExpanded.value = false;
116
+ emit('update:should-close', false);
117
+ };
118
+
119
+ const handleClear = () => {
120
+ searchQuery.value = "";
121
+ emit("update:modelValue", "");
122
+ emit("clear");
123
+ };
124
+
125
+ watch(() => props.modelValue, (newValue: string) => {
126
+ if (newValue !== searchQuery.value) {
127
+ searchQuery.value = newValue;
128
+ }
129
+ });
130
+
131
+ // Watch for shouldClose prop changes
132
+ watch(() => props.shouldClose, (newValue) => {
133
+ if (newValue) {
134
+ closeSearch();
135
+ emit('update:should-close', false);
136
+ }
137
+ });
138
+
139
+ // Add onMounted hook
140
+ onMounted(() => {
141
+ isMounted.value = true;
142
+ // Forzar un reflow para asegurar que las transiciones estén listas
143
+ nextTick(() => {
144
+ if (originalContainer.value) {
145
+ originalContainer.value.offsetHeight;
146
+ }
147
+ });
148
+ });
149
+ </script>
150
+
151
+ <template>
152
+ <!-- Overlay -->
153
+ <Transition
154
+ enter-active-class="dsEcom-transition-opacity dsEcom-duration-300 dsEcom-ease-in-out"
155
+ leave-active-class="dsEcom-transition-opacity dsEcom-duration-300 dsEcom-ease-in-out"
156
+ enter-from-class="dsEcom-opacity-0"
157
+ leave-to-class="dsEcom-opacity-0"
158
+ >
159
+ <div
160
+ v-show="isMounted && isExpanded"
161
+ :class="[
162
+ 'dsEcom-fixed dsEcom-inset-0 dsEcom-w-screen dsEcom-h-screen',
163
+ overlayBgClass,
164
+ overlayOpacityClass
165
+ ]"
166
+ @click="closeSearch"
167
+ ></div>
168
+ </Transition>
169
+
170
+ <!-- Contenedor del input -->
171
+ <div class="dsEcom-relative dsEcom-transform-gpu">
172
+ <!-- Container principal -->
173
+ <div
174
+ ref="originalContainer"
175
+ :class="[
176
+ 'dsEcom-inline-flex dsEcom-items-center',
177
+ 'dsEcom-transition-transform dsEcom-transition-width dsEcom-duration-300 dsEcom-ease-in-out',
178
+ { 'dsEcom-transition-active': isTransitioning },
179
+ isExpanded ? [
180
+ 'dsEcom-absolute dsEcom-z-[60]',
181
+ props.expandedBgClass,
182
+ props.expandedShadowClass
183
+ ] : 'dsEcom-relative'
184
+ ]"
185
+ :style="{
186
+ width: isExpanded ? props.expandedWidth : inputWidth,
187
+ maxWidth: isExpanded ? '90vw' : 'none'
188
+ }"
189
+ @transitionend="isTransitioning = false"
190
+ >
191
+ <div class="dsEcom-relative dsEcom-w-full">
192
+ <div class="dsEcom-flex dsEcom-items-center">
193
+ <div class="dsEcom-relative dsEcom-w-full">
194
+ <input
195
+ v-model="searchQuery"
196
+ type="text"
197
+ :placeholder="placeholder"
198
+ :disabled="isDisabled"
199
+ :style="{ height: inputHeight }"
200
+ :class="[
201
+ inputClasses,
202
+ 'dsEcom-w-full dsEcom-pr-24'
203
+ ]"
204
+ @focus="handleFocus"
205
+ @input="handleInput"
206
+ @keydown="(e) => e.key === 'Enter' && handleSearch()"
207
+ :autofocus="isExpanded"
208
+ />
209
+ <!-- Botón Clear (X) -->
210
+ <button
211
+ v-if="showClearButton"
212
+ @click="handleClear"
213
+ type="button"
214
+ class="dsEcom-absolute dsEcom-right-12 dsEcom-top-1/2 dsEcom-transform dsEcom--translate-y-1/2 dsEcom-text-neutral-400 hover:dsEcom-text-neutral-600 dsEcom-transition-colors dsEcom-duration-200"
215
+ aria-label="Limpiar búsqueda"
216
+ >
217
+ <span class="dsEcom-text-xl">&times;</span>
218
+ </button>
219
+ <!-- Botón Search -->
220
+ <button
221
+ :id="searchButtonId"
222
+ @click="handleSearch"
223
+ type="button"
224
+ :class="[
225
+ searchButtonClasses,
226
+ 'dsEcom-absolute dsEcom-right-0 dsEcom-top-0 dsEcom-bottom-0 dsEcom-flex dsEcom-items-center dsEcom-justify-center'
227
+ ]"
228
+ :style="{
229
+ minWidth: inputHeight,
230
+ height: inputHeight
231
+ }"
232
+ aria-label="Buscar"
233
+ >
234
+ <LauEcomUpcIconSearch
235
+ :width="searchIconSize.width"
236
+ :height="searchIconSize.height"
237
+ :class="searchIconClass"
238
+ />
239
+ </button>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </template>
246
+
247
+ <style scoped>
248
+ [v-cloak] {
249
+ display: none;
250
+ }
251
+
252
+ .dsEcom-transform-gpu {
253
+ transform: translateZ(0);
254
+ backface-visibility: hidden;
255
+ perspective: 1000px;
256
+ will-change: transform, opacity, width;
257
+ }
258
+
259
+ .dsEcom-transition-transform {
260
+ transition-property: transform;
261
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
262
+ transition-duration: 300ms;
263
+ }
264
+
265
+ .dsEcom-transition-width {
266
+ transition-property: width;
267
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
268
+ transition-duration: 300ms;
269
+ }
270
+
271
+ .dsEcom-transition-active {
272
+ transition-property: all;
273
+ }
274
+
275
+ .dsEcom-opacity-0 {
276
+ opacity: 0;
277
+ }
278
+
279
+ .dsEcom-fixed.dsEcom-inset-0 {
280
+ position: fixed;
281
+ top: 0;
282
+ right: 0;
283
+ bottom: 0;
284
+ left: 0;
285
+ }
286
+
287
+ .dsEcom-w-screen {
288
+ width: 100vw;
289
+ }
290
+
291
+ .dsEcom-h-screen {
292
+ height: 100vh;
293
+ }
294
+
295
+ .dsEcom-transition-all {
296
+ transition-property: all;
297
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
298
+ transition-duration: 300ms;
299
+ }
300
+
301
+ .dsEcom-transition-opacity {
302
+ transition-property: opacity;
303
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
304
+ transition-duration: 300ms;
305
+ }
306
+
307
+ [v-show] {
308
+ transition: opacity 0.3s ease-in-out;
309
+ }
310
+
311
+ [v-show="false"] {
312
+ opacity: 0 !important;
313
+ pointer-events: none;
314
+ }
315
+
316
+ [v-show="true"] {
317
+ opacity: 1;
318
+ }
319
+
320
+ /* Asegurar que el contenido también tenga transición suave */
321
+ .dsEcom-px-4 {
322
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
323
+ }
324
+ </style>
@@ -0,0 +1,279 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, watch } from "vue";
3
+ import {
4
+ LauEcomUpcIconSearch,
5
+ LauEcomUpcIconNavArrow,
6
+ LauEcomUpcIconClose,
7
+ } from "../LauEcomIcon";
8
+
9
+ interface Props {
10
+ placeholder?: string;
11
+ isDisabled?: boolean;
12
+ modelValue?: string;
13
+ inputHeight?: string;
14
+ inputClass?: string;
15
+ inputBorderClass?: string;
16
+ inputBgClass?: string;
17
+ inputTextClass?: string;
18
+
19
+ // Botón de búsqueda props
20
+ searchButtonClass?: string;
21
+ searchButtonBgClass?: string;
22
+ searchButtonTextClass?: string;
23
+ searchIconSize?: {
24
+ width: string;
25
+ height: string;
26
+ };
27
+ searchIconClass?: string;
28
+
29
+ // Props para botón móvil
30
+ mobileButtonClass?: string;
31
+ mobileButtonBgClass?: string;
32
+ mobileButtonTextClass?: string;
33
+ mobileIconSize?: {
34
+ width: string;
35
+ height: string;
36
+ };
37
+
38
+ // Props para flecha de retorno
39
+ arrowButtonClass?: string;
40
+ arrowButtonBgClass?: string;
41
+ arrowIconClass?: string;
42
+ arrowIconSize?: {
43
+ width: string;
44
+ height: string;
45
+ };
46
+
47
+ // Props para panel móvil
48
+ mobilePanelBgClass?: string;
49
+ mobilePanelClass?: string;
50
+ }
51
+
52
+ const props = withDefaults(defineProps<Props>(), {
53
+ placeholder: "Buscar...",
54
+ isDisabled: false,
55
+ modelValue: "",
56
+ inputHeight: "40px",
57
+ inputClass: "dsEcom-rounded-lg",
58
+ inputBorderClass: "dsEcom-border dsEcom-border-neutral-80",
59
+ inputBgClass: "dsEcom-bg-white",
60
+ inputTextClass: "dsEcom-text-neutral-900",
61
+
62
+ searchButtonClass: "dsEcom-rounded-r-lg",
63
+ searchButtonBgClass: "bg-base-color-primary-20",
64
+ searchButtonTextClass: "text-base-color-primary-60",
65
+ searchIconSize: () => ({ width: "20", height: "20" }),
66
+ searchIconClass: "",
67
+
68
+ mobileButtonClass: "dsEcom-rounded-lg",
69
+ mobileButtonBgClass: "bg-base-color-primary-20",
70
+ mobileButtonTextClass: "text-base-color-primary-60",
71
+ mobileIconSize: () => ({ width: "24", height: "24" }),
72
+
73
+ arrowButtonClass: "dsEcom-rounded-lg",
74
+ arrowButtonBgClass: "bg-base-color-neutral-10",
75
+ arrowIconClass: "text-[#00A08C]",
76
+ arrowIconSize: () => ({ width: "24", height: "24" }),
77
+
78
+ mobilePanelBgClass: "dsEcom-bg-white",
79
+ mobilePanelClass: "dsEcom-p-4"
80
+ });
81
+
82
+ const emit = defineEmits({
83
+ "update:modelValue": (value: string) => true,
84
+ "search": (value: string) => true,
85
+ "click:search-icon": () => true
86
+ });
87
+
88
+ const searchQuery = ref(props.modelValue);
89
+ const isSearchOpen = ref(false);
90
+ const searchPanel = ref<HTMLElement | null>(null);
91
+
92
+ const inputClasses = computed(() => [
93
+ 'dsEcom-w-full dsEcom-transition-all dsEcom-duration-300',
94
+ props.inputClass,
95
+ props.inputBorderClass,
96
+ props.inputBgClass,
97
+ props.inputTextClass
98
+ ]);
99
+
100
+ const searchButtonClasses = computed(() => [
101
+ 'dsEcom-flex dsEcom-items-center dsEcom-px-3 dsEcom-transition-all dsEcom-duration-300',
102
+ props.searchButtonClass,
103
+ props.searchButtonBgClass,
104
+ props.searchButtonTextClass
105
+ ]);
106
+
107
+ const handleSearch = () => {
108
+ if (searchQuery.value?.trim()) {
109
+ emit("search", searchQuery.value);
110
+ emit("update:modelValue", searchQuery.value);
111
+ emit("click:search-icon");
112
+ handleSearchClose();
113
+ }
114
+ };
115
+
116
+ const handleInput = () => {
117
+ emit("update:modelValue", searchQuery.value);
118
+ };
119
+
120
+ const handleSearchOpen = () => {
121
+ isSearchOpen.value = true;
122
+ };
123
+
124
+ const handleSearchClose = () => {
125
+ isSearchOpen.value = false;
126
+ setTimeout(() => {
127
+ searchQuery.value = '';
128
+ emit('update:modelValue', '');
129
+ }, 300);
130
+ };
131
+
132
+ const clearSearch = () => {
133
+ searchQuery.value = '';
134
+ emit('update:modelValue', '');
135
+ };
136
+
137
+ watch(() => props.modelValue, (newValue: string) => {
138
+ if (newValue !== searchQuery.value) {
139
+ searchQuery.value = newValue;
140
+ }
141
+ });
142
+ </script>
143
+
144
+ <template>
145
+ <div class="dsEcom-relative dsEcom-transform-gpu">
146
+ <!-- Botón de búsqueda -->
147
+ <button
148
+ :class="[
149
+ 'dsEcom-fixed dsEcom-right-4 dsEcom-top-4 dsEcom-z-50',
150
+ 'dsEcom-p-2 dsEcom-rounded-lg dsEcom-transition-opacity dsEcom-duration-300 dsEcom-ease-in-out',
151
+ mobileButtonClass,
152
+ mobileButtonBgClass,
153
+ 'text-base-color-primary-60',
154
+ isSearchOpen ? 'dsEcom-opacity-0 dsEcom-pointer-events-none' : 'dsEcom-opacity-100'
155
+ ]"
156
+ @click="handleSearchOpen"
157
+ >
158
+ <LauEcomUpcIconSearch
159
+ :width="mobileIconSize.width"
160
+ :height="mobileIconSize.height"
161
+ class="text-current"
162
+ />
163
+ </button>
164
+
165
+ <!-- Panel de búsqueda -->
166
+ <div
167
+ ref="searchPanel"
168
+ :class="[
169
+ 'dsEcom-fixed dsEcom-right-4 dsEcom-top-4 dsEcom-z-50',
170
+ 'dsEcom-transition-all dsEcom-duration-300 dsEcom-ease-in-out',
171
+ mobilePanelBgClass,
172
+ mobilePanelClass,
173
+ isSearchOpen ? 'dsEcom-w-[calc(100%-2rem)] dsEcom-opacity-100' : 'dsEcom-w-12 dsEcom-opacity-0 dsEcom-pointer-events-none'
174
+ ]"
175
+ >
176
+ <div class="dsEcom-flex dsEcom-items-center dsEcom-gap-2 dsEcom-h-12">
177
+ <button
178
+ @click="handleSearchClose"
179
+ :class="[
180
+ 'dsEcom-p-2 dsEcom-rounded-lg dsEcom-transition-colors dsEcom-duration-300 dsEcom-ease-in-out',
181
+ arrowButtonClass,
182
+ arrowButtonBgClass,
183
+ 'text-base-color-primary-60'
184
+ ]"
185
+ >
186
+ <LauEcomUpcIconNavArrow
187
+ :width="arrowIconSize.width"
188
+ :height="arrowIconSize.height"
189
+ class="dsEcom-transform dsEcom--rotate-90 text-current"
190
+ />
191
+ </button>
192
+
193
+ <div class="dsEcom-flex-1 dsEcom-overflow-hidden">
194
+ <div class="dsEcom-relative">
195
+ <input
196
+ v-model="searchQuery"
197
+ type="text"
198
+ :placeholder="placeholder"
199
+ :disabled="isDisabled"
200
+ :style="{ height: inputHeight }"
201
+ :class="[
202
+ inputClasses,
203
+ 'dsEcom-w-full dsEcom-pr-24',
204
+ 'dsEcom-transition-colors dsEcom-duration-300 dsEcom-ease-in-out'
205
+ ]"
206
+ @input="handleInput"
207
+ @keyup.enter="handleSearch"
208
+ :autofocus="isSearchOpen"
209
+ />
210
+ <!-- Botón de limpiar -->
211
+ <button
212
+ v-if="searchQuery"
213
+ @click="clearSearch"
214
+ :class="[
215
+ 'dsEcom-absolute dsEcom-right-12 dsEcom-top-1/2 dsEcom-transform dsEcom--translate-y-1/2 dsEcom-px-2',
216
+ 'dsEcom-transition-colors dsEcom-duration-300 dsEcom-ease-in-out',
217
+ 'text-base-color-primary-60'
218
+ ]"
219
+ >
220
+ <LauEcomUpcIconClose
221
+ :width="searchIconSize.width"
222
+ :height="searchIconSize.height"
223
+ class="text-current"
224
+ />
225
+ </button>
226
+ <!-- Botón de búsqueda -->
227
+ <button
228
+ @click="handleSearch"
229
+ :class="[
230
+ searchButtonClasses,
231
+ 'dsEcom-absolute dsEcom-right-0 dsEcom-top-1/2 dsEcom-transform dsEcom--translate-y-1/2 dsEcom-px-3',
232
+ 'dsEcom-transition-colors dsEcom-duration-300 dsEcom-ease-in-out',
233
+ 'text-base-color-primary-60'
234
+ ]"
235
+ >
236
+ <LauEcomUpcIconSearch
237
+ :width="searchIconSize.width"
238
+ :height="searchIconSize.height"
239
+ class="text-current"
240
+ />
241
+ </button>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </template>
248
+
249
+ <style scoped>
250
+ .dsEcom-transform-gpu {
251
+ transform: translateZ(0);
252
+ backface-visibility: hidden;
253
+ perspective: 1000px;
254
+ will-change: opacity, width;
255
+ }
256
+
257
+ /* Transiciones base */
258
+ .dsEcom-transition-all {
259
+ transition-property: all;
260
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
261
+ transition-duration: 300ms;
262
+ }
263
+
264
+ /* Input styles */
265
+ input {
266
+ transition-property: background-color, border-color, color;
267
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
268
+ transition-duration: 300ms;
269
+ }
270
+
271
+ /* Efectos hover para botones */
272
+ button:hover {
273
+ opacity: 0.8;
274
+ }
275
+
276
+ button:active {
277
+ opacity: 0.6;
278
+ }
279
+ </style>