@milaboratories/uikit 2.2.36 → 2.2.38

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,132 @@
1
+ <script lang="ts" setup>
2
+ import { tapIf } from '@milaboratories/helpers';
3
+ import { computed } from 'vue';
4
+ import type { PlChartStackedBarSegment } from './types';
5
+
6
+ const props = defineProps<{
7
+ value: PlChartStackedBarSegment[];
8
+ height?: number;
9
+ showFractionInLabel?: boolean;
10
+ }>();
11
+
12
+ const style = computed(() => ({
13
+ height: '100%',
14
+ minHeight: '82px',
15
+ }));
16
+
17
+ const parts = computed(() => {
18
+ const parts = props.value || [];
19
+ const total = parts.reduce((res, it) => res + it.value, 0);
20
+ return parts.map((p) => {
21
+ const fraction = (p.value / total) * 100;
22
+ const fractionString = ((p.value / total) * 100).toFixed(1);
23
+ const label = tapIf(p.label, (label) => {
24
+ if (props.showFractionInLabel) {
25
+ return `${label} ${fractionString}%`;
26
+ }
27
+
28
+ return label;
29
+ });
30
+ return {
31
+ color: p.color.toString(),
32
+ description: p.description,
33
+ fraction,
34
+ label,
35
+ };
36
+ });
37
+ });
38
+ </script>
39
+
40
+ <template>
41
+ <div
42
+ :class="[$style.component]" :style="style"
43
+ >
44
+ <div :class="$style.track">
45
+ <div
46
+ v-for="(v, i) in [0, 25, 50, 75, 100]"
47
+ :key="i"
48
+ :style="`left: ${v}%`"
49
+ :data-content="`${v}%`"
50
+ />
51
+ </div>
52
+ <div :class="$style.container">
53
+ <div v-if="!parts.length" :class="$style.notReady">Not ready</div>
54
+ <div
55
+ v-for="(p, i) in parts"
56
+ :key="i"
57
+ :title.prop="p.description ?? p.label"
58
+ :style="{
59
+ width: `${p.fraction}%`,
60
+ backgroundColor: p.color
61
+ }"
62
+ />
63
+ </div>
64
+ </div>
65
+ </template>
66
+
67
+ <style lang="scss" module>
68
+ .component {
69
+ height: auto;
70
+ position: relative;
71
+ overflow: visible;
72
+ border: 1px solid green;
73
+
74
+ border: 1px solid var(--txt-01);
75
+ padding: 12px 6px;
76
+ border-radius: 0;
77
+ margin-bottom: 24px;
78
+ }
79
+
80
+ .container {
81
+ display: flex;
82
+ flex-direction: row;
83
+ gap: 1px;
84
+ width: 100%;
85
+ height: 100%;
86
+ align-items: center;
87
+ overflow: hidden;
88
+ > div {
89
+ height: 100%;
90
+ display: flex;
91
+ align-items: center;
92
+ }
93
+ }
94
+
95
+ .track {
96
+ position: absolute;
97
+ top: 0;
98
+ left: 6px;
99
+ right: 6px;
100
+ bottom: 0;
101
+ /* z-index: 1; */
102
+ > div {
103
+ height: 100%;
104
+ position: absolute;
105
+ bottom: 0;
106
+ border-left: 1px solid #e1e3eb;
107
+ &::after {
108
+ position: absolute;
109
+ content: attr(data-content);
110
+ left: 0;
111
+ bottom: -24px;
112
+ transform: translateX(-50%);
113
+ }
114
+ }
115
+ }
116
+
117
+ .notReady {
118
+ color: var(--txt-03) !important;
119
+ }
120
+
121
+ .component .notReady {
122
+ font-size: larger;
123
+ font-weight: bolder;
124
+ color: var(--txt-mask);
125
+ margin-left: 24px;
126
+ }
127
+
128
+ .component .container {
129
+ position: relative;
130
+ z-index: 1;
131
+ }
132
+ </style>
@@ -0,0 +1,3 @@
1
+ export { default as PlChartStackedBar } from './PlChartStackedBar.vue';
2
+
3
+ export * from './types';
@@ -0,0 +1,34 @@
1
+ import type { Color } from '@/colors';
2
+
3
+ export type PlChartStackedBarSegment = {
4
+ value: number;
5
+ label: string;
6
+ description?: string;
7
+ color: string | Color;
8
+ };
9
+
10
+ export type PlChartStackedBarSettings = {
11
+ /**
12
+ * The title of the chart.
13
+ * This will be displayed at the top of the chart, if provided.
14
+ */
15
+ title?: string;
16
+
17
+ /**
18
+ * The data to be displayed in the chart.
19
+ * Each entry represents a segment of a stacked bar.
20
+ */
21
+ data: PlChartStackedBarSegment[];
22
+
23
+ /**
24
+ * The maximum number of legends displayed in a single column.
25
+ * Defaults to 5 if not specified.
26
+ */
27
+ maxLegendsInColumn?: number;
28
+
29
+ /**
30
+ * Whether to show legends for the chart.
31
+ * Defaults to `true` if not specified.
32
+ */
33
+ showLegends?: boolean;
34
+ };
@@ -0,0 +1,282 @@
1
+ import type { Ref } from 'vue';
2
+ import { computed, watch, watchEffect } from 'vue';
3
+ import { useEventListener } from './useEventListener';
4
+
5
+ type SortableItem = {
6
+ el: HTMLElement;
7
+ y: number;
8
+ dy: number;
9
+ orderChanged: boolean;
10
+ initialScrollTop: number;
11
+ };
12
+
13
+ export type SortableSettings = {
14
+ onChange: (indices: number[]) => void;
15
+ handle?: string;
16
+ shakeBuffer?: number;
17
+ reorderDelay?: number;
18
+ transitionDelay?: string;
19
+ };
20
+
21
+ const classes = {
22
+ item: 'sortable__item',
23
+ animate: 'sortable__animate',
24
+ };
25
+
26
+ const getOffset = (el: HTMLElement) => {
27
+ return el.getBoundingClientRect().y;
28
+ };
29
+
30
+ const getMiddle = (el: HTMLElement) => {
31
+ const { y, height } = el.getBoundingClientRect();
32
+ return y + Math.ceil(height / 2);
33
+ };
34
+
35
+ const getBottom = (el: HTMLElement) => {
36
+ const { y, height } = el.getBoundingClientRect();
37
+ return y + height;
38
+ };
39
+
40
+ /**
41
+ * Description: Scroll support has been added to the container where sorting takes place.
42
+ * Some functionality duplicates the behavior of useScrollable.
43
+ *
44
+ * Purpose: To enable automatic scrolling when dragging items beyond the visible area of the container.
45
+ * Future Plan:
46
+ * - Verify the behavior of the new scrolling functionality within blocks and text elements.
47
+ * - Merge the current implementation with useScrollable to eliminate code duplication and create a unified scrolling solution.
48
+ */
49
+ export function useSortable2(listRef: Ref<HTMLElement | undefined>, settings: SortableSettings) {
50
+ const state = {
51
+ item: undefined as SortableItem | undefined,
52
+ options() {
53
+ return [...(listRef.value?.children ?? [])] as HTMLElement[];
54
+ },
55
+ };
56
+ let oldScrollTop = 0;
57
+
58
+ watch(() => listRef.value, () => {
59
+ setTimeout(() => {
60
+ if (listRef.value) {
61
+ listRef.value.scrollTop = oldScrollTop;
62
+ }
63
+ }, 0);
64
+ });
65
+
66
+ const optionsRef = computed(() => {
67
+ return state.options();
68
+ });
69
+
70
+ const shakeBuffer = settings.shakeBuffer ?? 10;
71
+
72
+ const reorderDelay = settings.reorderDelay ?? 100;
73
+
74
+ function mouseDown(this: HTMLElement, e: { y: number; target: EventTarget | null }) {
75
+ const handle = settings.handle ? this.querySelector(settings.handle) : null;
76
+
77
+ if (!handle) {
78
+ return;
79
+ }
80
+
81
+ if (handle && !handle.contains(e.target as HTMLElement)) {
82
+ return;
83
+ }
84
+
85
+ this.classList.remove(classes.animate);
86
+ this.classList.add(classes.item);
87
+
88
+ state.item = {
89
+ el: this,
90
+ y: e.y,
91
+ dy: 0,
92
+ initialScrollTop: listRef.value?.scrollTop || 0,
93
+ orderChanged: false,
94
+ };
95
+ }
96
+
97
+ function elementsBefore(el: HTMLElement) {
98
+ const options = state.options();
99
+ return options.slice(0, options.indexOf(el));
100
+ }
101
+
102
+ function elementsAfter(el: HTMLElement) {
103
+ const children = state.options();
104
+ return children.slice(children.indexOf(el) + 1);
105
+ }
106
+
107
+ function insertBefore(before: HTMLElement, el: HTMLElement) {
108
+ const children = state.options().filter((e) => e !== el);
109
+ const index = children.indexOf(before);
110
+ children.splice(index, 0, el);
111
+ return children;
112
+ }
113
+
114
+ function insertAfter(after: HTMLElement, el: HTMLElement) {
115
+ const children = state.options().filter((e) => e !== el);
116
+ const index = children.indexOf(after);
117
+ children.splice(index + 1, 0, el);
118
+ return children;
119
+ }
120
+
121
+ function updatePosition(item: SortableItem, y: number) {
122
+ const currentScrollTop = listRef.value?.scrollTop || 0;
123
+ const scrollDiff = currentScrollTop - (item.initialScrollTop || 0);
124
+ item.dy = y - item.y + scrollDiff;
125
+ item.el.style.setProperty('transform', `translateY(${item.dy}px)`);
126
+ }
127
+
128
+ function changeOrder(reordered: HTMLElement[]) {
129
+ if (!state.item) {
130
+ return;
131
+ }
132
+
133
+ const { el } = state.item;
134
+
135
+ if (!el.isConnected) {
136
+ state.item = undefined;
137
+ return;
138
+ }
139
+
140
+ const oldPositions = reordered.map((e) => getOffset(e));
141
+
142
+ const y1 = getOffset(el);
143
+ listRef.value?.replaceChildren(...reordered);
144
+ const y2 = getOffset(el);
145
+
146
+ const newPositions = reordered.map((e) => getOffset(e));
147
+
148
+ const toAnimate: HTMLElement[] = [];
149
+
150
+ for (let i = 0; i < newPositions.length; i++) {
151
+ const option = reordered[i];
152
+
153
+ if (option === state.item.el) {
154
+ continue;
155
+ }
156
+
157
+ const newY = newPositions[i];
158
+
159
+ const oldY = oldPositions[i];
160
+
161
+ const invert = oldY - newY;
162
+
163
+ option.style.transform = `translateY(${invert}px)`;
164
+
165
+ toAnimate.push(option);
166
+ }
167
+
168
+ const dy = y2 - y1;
169
+
170
+ state.item.y = state.item.y + dy;
171
+ state.item.dy = state.item.dy - dy;
172
+ state.item.orderChanged = true;
173
+ state.item.el.style.setProperty('transform', `translateY(${state.item.dy}px)`);
174
+
175
+ toAnimate.forEach((o) => o.classList.remove(classes.animate));
176
+
177
+ requestAnimationFrame(function () {
178
+ toAnimate.forEach((option) => {
179
+ option.classList.add(classes.animate);
180
+ option.style.transform = '';
181
+ option.addEventListener('transitionend', () => {
182
+ option.classList.remove(classes.animate);
183
+ });
184
+ });
185
+ });
186
+ }
187
+
188
+ useEventListener(window, 'mousemove', (e: { y: number }) => {
189
+ if (!state.item) {
190
+ return;
191
+ }
192
+
193
+ const { el } = state.item;
194
+
195
+ updatePosition(state.item, e.y);
196
+
197
+ const upper = getOffset(state.item.el);
198
+ const bottom = getBottom(state.item.el);
199
+
200
+ const before = elementsBefore(el);
201
+ const after = elementsAfter(el);
202
+
203
+ before.forEach((e) => {
204
+ const y = getMiddle(e);
205
+
206
+ if (upper + shakeBuffer < y) {
207
+ changeOrder(insertBefore(e, el));
208
+ }
209
+ });
210
+
211
+ after.forEach((e) => {
212
+ const y = getMiddle(e);
213
+
214
+ if (bottom - shakeBuffer > y) {
215
+ changeOrder(insertAfter(e, el));
216
+ }
217
+ });
218
+
219
+ if (listRef.value) {
220
+ const rect = listRef.value.getBoundingClientRect();
221
+
222
+ const deltaUp = rect.top + el.getBoundingClientRect().height / 2;
223
+ if ((e as MouseEvent).clientY < deltaUp) {
224
+ listRef.value.scrollTop += (e as MouseEvent).clientY - deltaUp;
225
+ }
226
+
227
+ const deltaDown = rect.bottom - el.getBoundingClientRect().height / 2;
228
+ if ((e as MouseEvent).clientY > deltaDown) {
229
+ listRef.value.scrollTop += (e as MouseEvent).clientY - deltaDown;
230
+ }
231
+ }
232
+ });
233
+
234
+ useEventListener(window, 'mouseup', () => {
235
+ if (!state.item) {
236
+ return;
237
+ }
238
+
239
+ oldScrollTop = listRef.value?.scrollTop || 0;
240
+
241
+ const { el, orderChanged } = state.item;
242
+
243
+ el.classList.add(classes.animate);
244
+ el.style.removeProperty('transform');
245
+
246
+ el.addEventListener('transitionend', () => {
247
+ el.classList.remove(classes.animate, classes.item);
248
+ });
249
+
250
+ setTimeout(() => {
251
+ if (!orderChanged) {
252
+ return;
253
+ }
254
+
255
+ const newIndices = state.options().map((o) => Number(o.getAttribute('data-index')));
256
+
257
+ const list = listRef.value;
258
+
259
+ if (list) {
260
+ for (const child of state.options()) {
261
+ list.removeChild(child);
262
+ }
263
+
264
+ optionsRef.value.forEach((child) => {
265
+ list.appendChild(child);
266
+ });
267
+ }
268
+
269
+ settings.onChange(newIndices);
270
+ }, reorderDelay);
271
+
272
+ state.item = undefined;
273
+ });
274
+
275
+ watchEffect(() => {
276
+ optionsRef.value.forEach((child, i) => {
277
+ child.removeEventListener('mousedown', mouseDown);
278
+ child.addEventListener('mousedown', mouseDown);
279
+ child.setAttribute('data-index', String(i));
280
+ });
281
+ });
282
+ }
package/src/index.ts CHANGED
@@ -60,6 +60,10 @@ export * from './components/PlMaskIcon24';
60
60
  export * from './components/PlIcon16';
61
61
  export * from './components/PlIcon24';
62
62
 
63
+ export * from './components/PlChartStackedBar';
64
+
65
+ export * from './colors';
66
+
63
67
  // @TODO review (may be private)
64
68
  import DropdownListItem from './components/DropdownListItem.vue';
65
69
 
@@ -82,6 +86,7 @@ export { useMouseCapture } from './composition/useMouseCapture';
82
86
  export { useHover } from './composition/useHover';
83
87
  export { useMouse } from './composition/useMouse';
84
88
  export { useSortable } from './composition/useSortable';
89
+ export { useSortable2 } from './composition/useSortable2';
85
90
  export { useInterval } from './composition/useInterval';
86
91
  export { useFormState } from './composition/useFormState';
87
92
  export { useQuery } from './composition/useQuery.ts';