dxd-style-code 0.1.13 → 0.1.15
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.
- package/dist/dxd-style-code.js +3057 -2971
- package/dist/dxd-style-code.umd.cjs +3 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/molecules/DXSegmentedControl/DXSegmentedControl.stories.js +230 -0
- package/src/components/molecules/DXSegmentedControl/DXSegmentedControl.vue +253 -38
- package/src/components/molecules/DXTableFiltersPanel/index.js +2 -0
|
@@ -1,52 +1,93 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="flex-col"
|
|
4
|
+
:class="scrollable ? 'flex' : 'inline-flex'"
|
|
5
|
+
:style="scrollable && maxWidth ? { maxWidth, width: '100%' } : {}"
|
|
6
|
+
data-component="DXSegmentedControl"
|
|
7
|
+
:data-scrollable="scrollable"
|
|
8
|
+
>
|
|
3
9
|
<p v-if="label" class="text-sm font-medium text-slate-700 mb-2">{{ label }}</p>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
<!-- Wrapper для градиентов -->
|
|
12
|
+
<div
|
|
13
|
+
class="relative w-full"
|
|
14
|
+
:class="scrollable && 'scroll-wrapper'"
|
|
15
|
+
>
|
|
16
|
+
<!-- Левый градиент -->
|
|
17
|
+
<div
|
|
18
|
+
v-if="scrollable && showFade"
|
|
19
|
+
class="fade-left"
|
|
20
|
+
:class="canScrollLeft ? 'opacity-100' : 'opacity-0'"
|
|
9
21
|
/>
|
|
10
22
|
|
|
11
|
-
<!--
|
|
12
|
-
<
|
|
13
|
-
v-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
<!-- Правый градиент -->
|
|
24
|
+
<div
|
|
25
|
+
v-if="scrollable && showFade"
|
|
26
|
+
class="fade-right"
|
|
27
|
+
:class="canScrollRight ? 'opacity-100' : 'opacity-0'"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<!-- Scrollable container -->
|
|
31
|
+
<div
|
|
32
|
+
ref="containerRef"
|
|
33
|
+
class="relative p-1 bg-slate-100 rounded-xl gap-1"
|
|
18
34
|
:class="[
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
disabled && 'opacity-60 cursor-not-allowed',
|
|
35
|
+
scrollable ? 'flex overflow-x-auto scrollbar-hide w-full' : 'inline-flex',
|
|
36
|
+
isDragging && 'cursor-grabbing select-none',
|
|
37
|
+
scrollable && !isDragging && 'cursor-grab'
|
|
23
38
|
]"
|
|
24
|
-
|
|
25
|
-
@
|
|
39
|
+
@mousedown="handleMouseDown"
|
|
40
|
+
@mousemove="handleMouseMove"
|
|
41
|
+
@mouseup="handleMouseUp"
|
|
42
|
+
@mouseleave="handleMouseUp"
|
|
43
|
+
@scroll="handleScroll"
|
|
26
44
|
>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
:animation="getIconAnimation(option)"
|
|
45
|
+
<!-- Floating indicator -->
|
|
46
|
+
<div
|
|
47
|
+
class="absolute bg-white rounded-lg shadow-sm transition-all duration-200 ease-out pointer-events-none"
|
|
48
|
+
:style="indicatorStyle"
|
|
32
49
|
/>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
:
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
|
|
51
|
+
<!-- Buttons -->
|
|
52
|
+
<button
|
|
53
|
+
v-for="(option, index) in options"
|
|
54
|
+
:key="option.value"
|
|
55
|
+
type="button"
|
|
56
|
+
:ref="el => { if (el) buttonRefs[index] = el }"
|
|
57
|
+
class="relative z-10 px-4 py-1.5 text-sm font-medium transition-colors duration-150 rounded-lg whitespace-nowrap min-w-[60px] text-center inline-flex items-center justify-center gap-1.5 flex-shrink-0"
|
|
58
|
+
:class="[
|
|
59
|
+
modelValue === option.value
|
|
60
|
+
? 'text-slate-900'
|
|
61
|
+
: 'text-slate-600 hover:text-slate-900',
|
|
62
|
+
disabled && 'opacity-60 cursor-not-allowed',
|
|
63
|
+
]"
|
|
64
|
+
:disabled="disabled"
|
|
65
|
+
@click="handleButtonClick(option.value, $event)"
|
|
40
66
|
>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
<DXIcon
|
|
68
|
+
v-if="option.icon"
|
|
69
|
+
:icon="option.icon"
|
|
70
|
+
size="xs"
|
|
71
|
+
:animation="getIconAnimation(option)"
|
|
72
|
+
/>
|
|
73
|
+
<span v-if="option.label">{{ option.label }}</span>
|
|
74
|
+
<span
|
|
75
|
+
v-if="option.count !== undefined && option.count !== null"
|
|
76
|
+
class="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-[11px] font-semibold rounded-full"
|
|
77
|
+
:class="modelValue === option.value
|
|
78
|
+
? 'bg-slate-800 text-white'
|
|
79
|
+
: 'bg-slate-200 text-slate-700'"
|
|
80
|
+
>
|
|
81
|
+
{{ option.count }}
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
44
85
|
</div>
|
|
45
86
|
</div>
|
|
46
87
|
</template>
|
|
47
88
|
|
|
48
89
|
<script setup>
|
|
49
|
-
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
|
90
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue";
|
|
50
91
|
import DXIcon from "../../atoms/DXIcon/DXIcon.vue";
|
|
51
92
|
|
|
52
93
|
const props = defineProps({
|
|
@@ -61,13 +102,40 @@ const props = defineProps({
|
|
|
61
102
|
/** Анимировать только активную иконку */
|
|
62
103
|
animateActiveOnly: { type: Boolean, default: true },
|
|
63
104
|
disabled: { type: Boolean, default: false },
|
|
105
|
+
/**
|
|
106
|
+
* Включить режим прокрутки (без скроллбара, с поддержкой drag-to-scroll)
|
|
107
|
+
* @default false
|
|
108
|
+
*/
|
|
109
|
+
scrollable: { type: Boolean, default: false },
|
|
110
|
+
/**
|
|
111
|
+
* Максимальная ширина контейнера (при scrollable: true)
|
|
112
|
+
* Может быть строкой CSS (например '300px', '100%', '50vw')
|
|
113
|
+
* @default null
|
|
114
|
+
*/
|
|
115
|
+
maxWidth: { type: String, default: null },
|
|
116
|
+
/**
|
|
117
|
+
* Показывать градиенты размытия по краям при scrollable
|
|
118
|
+
* @default true
|
|
119
|
+
*/
|
|
120
|
+
showFade: { type: Boolean, default: true },
|
|
64
121
|
});
|
|
65
122
|
|
|
66
123
|
const emit = defineEmits(["update:modelValue"]);
|
|
67
124
|
|
|
125
|
+
const containerRef = ref(null);
|
|
68
126
|
const buttonRefs = ref([]);
|
|
69
127
|
const indicatorStyle = ref({});
|
|
70
128
|
|
|
129
|
+
// Drag-to-scroll state
|
|
130
|
+
const isDragging = ref(false);
|
|
131
|
+
const startX = ref(0);
|
|
132
|
+
const scrollLeftPos = ref(0);
|
|
133
|
+
const hasDragged = ref(false);
|
|
134
|
+
|
|
135
|
+
// Scroll position state для градиентов
|
|
136
|
+
const canScrollLeft = ref(false);
|
|
137
|
+
const canScrollRight = ref(false);
|
|
138
|
+
|
|
71
139
|
const selectedIndex = computed(() => {
|
|
72
140
|
return props.options.findIndex((opt) => opt.value === props.modelValue);
|
|
73
141
|
});
|
|
@@ -76,10 +144,13 @@ const updateIndicator = () => {
|
|
|
76
144
|
const index = selectedIndex.value;
|
|
77
145
|
if (index >= 0 && buttonRefs.value[index]) {
|
|
78
146
|
const button = buttonRefs.value[index];
|
|
147
|
+
// В scrollable режиме корректируем позицию на величину скролла,
|
|
148
|
+
// т.к. absolute элемент не скроллируется вместе с контентом
|
|
149
|
+
const scrollOffset = props.scrollable && containerRef.value ? containerRef.value.scrollLeft : 0;
|
|
79
150
|
indicatorStyle.value = {
|
|
80
151
|
width: `${button.offsetWidth}px`,
|
|
81
152
|
height: `${button.offsetHeight}px`,
|
|
82
|
-
left: `${button.offsetLeft}px`,
|
|
153
|
+
left: `${button.offsetLeft - scrollOffset}px`,
|
|
83
154
|
top: `${button.offsetTop}px`,
|
|
84
155
|
};
|
|
85
156
|
}
|
|
@@ -105,12 +176,156 @@ const getIconAnimation = (option) => {
|
|
|
105
176
|
return props.iconAnimation;
|
|
106
177
|
};
|
|
107
178
|
|
|
179
|
+
// Обновление состояния градиентов
|
|
180
|
+
const updateScrollState = () => {
|
|
181
|
+
if (!containerRef.value) return;
|
|
182
|
+
|
|
183
|
+
const { scrollLeft, scrollWidth, clientWidth } = containerRef.value;
|
|
184
|
+
canScrollLeft.value = scrollLeft > 2; // Небольшой threshold
|
|
185
|
+
canScrollRight.value = scrollLeft < scrollWidth - clientWidth - 2;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Обработчик scroll события
|
|
189
|
+
const handleScroll = () => {
|
|
190
|
+
updateScrollState();
|
|
191
|
+
updateIndicator(); // Обновляем позицию индикатора при скролле
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Drag-to-scroll handlers
|
|
195
|
+
const handleMouseDown = (e) => {
|
|
196
|
+
if (!props.scrollable || !containerRef.value) return;
|
|
197
|
+
|
|
198
|
+
isDragging.value = true;
|
|
199
|
+
hasDragged.value = false;
|
|
200
|
+
startX.value = e.pageX - containerRef.value.offsetLeft;
|
|
201
|
+
scrollLeftPos.value = containerRef.value.scrollLeft;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleMouseMove = (e) => {
|
|
205
|
+
if (!isDragging.value || !containerRef.value) return;
|
|
206
|
+
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
const x = e.pageX - containerRef.value.offsetLeft;
|
|
209
|
+
const walk = (x - startX.value) * 1.5; // Множитель скорости прокрутки
|
|
210
|
+
|
|
211
|
+
// Если переместились более чем на 5px, считаем это drag
|
|
212
|
+
if (Math.abs(walk) > 5) {
|
|
213
|
+
hasDragged.value = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
containerRef.value.scrollLeft = scrollLeftPos.value - walk;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handleMouseUp = () => {
|
|
220
|
+
isDragging.value = false;
|
|
221
|
+
// Сбрасываем hasDragged с небольшой задержкой, чтобы click event успел проверить флаг
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
hasDragged.value = false;
|
|
224
|
+
}, 0);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Обработчик клика по кнопке с учетом drag
|
|
228
|
+
const handleButtonClick = (value, event) => {
|
|
229
|
+
// Если был drag, не выбираем элемент
|
|
230
|
+
if (hasDragged.value) {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
select(value);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Прокрутка к выбранному элементу
|
|
238
|
+
const scrollToSelected = () => {
|
|
239
|
+
if (!props.scrollable || !containerRef.value) return;
|
|
240
|
+
|
|
241
|
+
const index = selectedIndex.value;
|
|
242
|
+
if (index >= 0 && buttonRefs.value[index]) {
|
|
243
|
+
const button = buttonRefs.value[index];
|
|
244
|
+
const container = containerRef.value;
|
|
245
|
+
|
|
246
|
+
// Центрируем выбранный элемент
|
|
247
|
+
const scrollPosition = button.offsetLeft - (container.clientWidth / 2) + (button.offsetWidth / 2);
|
|
248
|
+
container.scrollTo({
|
|
249
|
+
left: scrollPosition,
|
|
250
|
+
behavior: 'smooth'
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
108
255
|
watch(() => props.modelValue, () => {
|
|
109
|
-
nextTick(
|
|
256
|
+
nextTick(() => {
|
|
257
|
+
updateIndicator();
|
|
258
|
+
scrollToSelected();
|
|
259
|
+
});
|
|
110
260
|
});
|
|
111
261
|
|
|
262
|
+
// Отслеживание изменения размера для обновления градиентов (ref для изоляции между экземплярами)
|
|
263
|
+
const resizeObserver = ref(null);
|
|
264
|
+
|
|
112
265
|
onMounted(() => {
|
|
113
|
-
nextTick(
|
|
266
|
+
nextTick(() => {
|
|
267
|
+
updateIndicator();
|
|
268
|
+
updateScrollState();
|
|
269
|
+
scrollToSelected(); // Центрируем выбранный элемент при монтировании
|
|
270
|
+
|
|
271
|
+
// ResizeObserver для отслеживания изменения размера контейнера
|
|
272
|
+
if (props.scrollable && containerRef.value && window.ResizeObserver) {
|
|
273
|
+
resizeObserver.value = new ResizeObserver(() => {
|
|
274
|
+
updateScrollState();
|
|
275
|
+
});
|
|
276
|
+
resizeObserver.value.observe(containerRef.value);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
onBeforeUnmount(() => {
|
|
282
|
+
if (resizeObserver.value) {
|
|
283
|
+
resizeObserver.value.disconnect();
|
|
284
|
+
resizeObserver.value = null;
|
|
285
|
+
}
|
|
114
286
|
});
|
|
115
287
|
</script>
|
|
116
288
|
|
|
289
|
+
<style scoped>
|
|
290
|
+
/* Скрытие скроллбара для режима прокрутки */
|
|
291
|
+
.scrollbar-hide {
|
|
292
|
+
-ms-overflow-style: none; /* IE и Edge */
|
|
293
|
+
scrollbar-width: none; /* Firefox */
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
297
|
+
display: none; /* Chrome, Safari, Opera */
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Wrapper для градиентов */
|
|
301
|
+
.scroll-wrapper {
|
|
302
|
+
position: relative;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Градиенты по краям */
|
|
306
|
+
.fade-left,
|
|
307
|
+
.fade-right {
|
|
308
|
+
position: absolute;
|
|
309
|
+
top: 0;
|
|
310
|
+
bottom: 0;
|
|
311
|
+
width: 24px;
|
|
312
|
+
pointer-events: none;
|
|
313
|
+
z-index: 20;
|
|
314
|
+
transition: opacity 150ms ease;
|
|
315
|
+
border-radius: 0.75rem; /* rounded-xl */
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.fade-left {
|
|
319
|
+
left: 0;
|
|
320
|
+
background: linear-gradient(to right, rgb(241 245 249 / 1) 0%, rgb(241 245 249 / 0) 100%);
|
|
321
|
+
border-top-right-radius: 0;
|
|
322
|
+
border-bottom-right-radius: 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.fade-right {
|
|
326
|
+
right: 0;
|
|
327
|
+
background: linear-gradient(to left, rgb(241 245 249 / 1) 0%, rgb(241 245 249 / 0) 100%);
|
|
328
|
+
border-top-left-radius: 0;
|
|
329
|
+
border-bottom-left-radius: 0;
|
|
330
|
+
}
|
|
331
|
+
</style>
|