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.
@@ -1,52 +1,92 @@
1
1
  <template>
2
- <div class="inline-flex" data-component="DXSegmentedControl">
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
- <div class="relative inline-flex p-1 bg-slate-100 rounded-xl gap-1">
5
- <!-- Floating indicator -->
6
- <div
7
- class="absolute bg-white rounded-lg shadow-sm transition-all duration-200 ease-out"
8
- :style="indicatorStyle"
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
- <!-- Buttons -->
12
- <button
13
- v-for="(option, index) in options"
14
- :key="option.value"
15
- type="button"
16
- :ref="el => { if (el) buttonRefs[index] = el }"
17
- 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"
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
- modelValue === option.value
20
- ? 'text-slate-900'
21
- : 'text-slate-600 hover:text-slate-900',
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
- :disabled="disabled"
25
- @click="select(option.value)"
37
+ :style="scrollable && maxWidth ? { maxWidth } : {}"
38
+ @mousedown="handleMouseDown"
39
+ @mousemove="handleMouseMove"
40
+ @mouseup="handleMouseUp"
41
+ @mouseleave="handleMouseUp"
42
+ @scroll="handleScroll"
26
43
  >
27
- <DXIcon
28
- v-if="option.icon"
29
- :icon="option.icon"
30
- size="xs"
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
- <span v-if="option.label">{{ option.label }}</span>
34
- <span
35
- v-if="option.count !== undefined && option.count !== null"
36
- class="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-[11px] font-semibold rounded-full"
37
- :class="modelValue === option.value
38
- ? 'bg-slate-800 text-white'
39
- : 'bg-slate-200 text-slate-700'"
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
- {{ option.count }}
42
- </span>
43
- </button>
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(updateIndicator);
247
+ nextTick(() => {
248
+ updateIndicator();
249
+ scrollToSelected();
250
+ });
110
251
  });
111
252
 
253
+ // Отслеживание изменения размера для обновления градиентов
254
+ let resizeObserver = null;
255
+
112
256
  onMounted(() => {
113
- nextTick(updateIndicator);
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>
@@ -7,3 +7,6 @@ export { default } from './DXTableFiltersPanel.vue';
7
7
 
8
8
 
9
9
 
10
+
11
+
12
+