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