@milaboratories/uikit 2.2.92 → 2.2.93

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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +735 -726
  2. package/.turbo/turbo-type-check.log +1 -1
  3. package/CHANGELOG.md +6 -0
  4. package/dist/components/PlElementList/PlElementList.vue.d.ts +70 -0
  5. package/dist/components/PlElementList/PlElementList.vue.d.ts.map +1 -0
  6. package/dist/components/PlElementList/PlElementList.vue.js +10 -0
  7. package/dist/components/PlElementList/PlElementList.vue.js.map +1 -0
  8. package/dist/components/PlElementList/PlElementList.vue2.js +240 -0
  9. package/dist/components/PlElementList/PlElementList.vue2.js.map +1 -0
  10. package/dist/components/PlElementList/PlElementList.vue3.js +13 -0
  11. package/dist/components/PlElementList/PlElementList.vue3.js.map +1 -0
  12. package/dist/components/PlElementList/PlElementListItem.vue.d.ts +57 -0
  13. package/dist/components/PlElementList/PlElementListItem.vue.d.ts.map +1 -0
  14. package/dist/components/PlElementList/PlElementListItem.vue.js +10 -0
  15. package/dist/components/PlElementList/PlElementListItem.vue.js.map +1 -0
  16. package/dist/components/PlElementList/PlElementListItem.vue2.js +106 -0
  17. package/dist/components/PlElementList/PlElementListItem.vue2.js.map +1 -0
  18. package/dist/components/PlElementList/PlElementListItem.vue3.js +39 -0
  19. package/dist/components/PlElementList/PlElementListItem.vue3.js.map +1 -0
  20. package/dist/components/PlElementList/index.d.ts +2 -0
  21. package/dist/components/PlElementList/index.d.ts.map +1 -0
  22. package/dist/components/PlElementList/utils.d.ts +3 -0
  23. package/dist/components/PlElementList/utils.d.ts.map +1 -0
  24. package/dist/components/PlElementList/utils.js +17 -0
  25. package/dist/components/PlElementList/utils.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +107 -105
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/util/helpers/dist/index.js +134 -105
  31. package/dist/lib/util/helpers/dist/index.js.map +1 -1
  32. package/dist/node_modules/.pnpm/@vueuse_core@13.3.0_vue@3.5.13_typescript@5.6.3_/node_modules/@vueuse/core/index.js +54 -44
  33. package/dist/node_modules/.pnpm/@vueuse_core@13.3.0_vue@3.5.13_typescript@5.6.3_/node_modules/@vueuse/core/index.js.map +1 -1
  34. package/dist/node_modules/.pnpm/@vueuse_integrations@13.3.0_axios@1.8.1_focus-trap@7.6.0_sortablejs@1.15.6_vue@3.5.13_typescript@5.6.3_/node_modules/@vueuse/integrations/useSortable.js +51 -0
  35. package/dist/node_modules/.pnpm/@vueuse_integrations@13.3.0_axios@1.8.1_focus-trap@7.6.0_sortablejs@1.15.6_vue@3.5.13_typescript@5.6.3_/node_modules/@vueuse/integrations/useSortable.js.map +1 -0
  36. package/dist/node_modules/.pnpm/@vueuse_shared@13.3.0_vue@3.5.13_typescript@5.6.3_/node_modules/@vueuse/shared/index.js +57 -49
  37. package/dist/node_modules/.pnpm/@vueuse_shared@13.3.0_vue@3.5.13_typescript@5.6.3_/node_modules/@vueuse/shared/index.js.map +1 -1
  38. package/dist/node_modules/.pnpm/sortablejs@1.15.6/node_modules/sortablejs/modular/sortable.esm.js +1262 -0
  39. package/dist/node_modules/.pnpm/sortablejs@1.15.6/node_modules/sortablejs/modular/sortable.esm.js.map +1 -0
  40. package/package.json +11 -8
  41. package/src/components/PlElementList/PlElementList.vue +336 -0
  42. package/src/components/PlElementList/PlElementListItem.vue +239 -0
  43. package/src/components/PlElementList/README.md +92 -0
  44. package/src/components/PlElementList/index.ts +1 -0
  45. package/src/components/PlElementList/utils.ts +17 -0
  46. package/src/index.ts +1 -0
@@ -0,0 +1,336 @@
1
+ <script generic="T extends unknown = unknown, K extends number | string = number | string" lang="ts" setup>
2
+ import type { ShallowRef } from 'vue';
3
+ import { computed, shallowRef, watch } from 'vue';
4
+ import { isNil, shallowHash } from '@milaboratories/helpers';
5
+ import { useSortable } from '@vueuse/integrations/useSortable';
6
+ import { type SortableEvent } from 'sortablejs';
7
+ import { moveElements, optionalUpdateRef } from './utils.ts';
8
+ import PlElementListItem from './PlElementListItem.vue';
9
+
10
+ const itemsRef = defineModel<T[]>('items', { required: true });
11
+ const draggableSetRef = defineModel<Set<T>>('draggableItems');
12
+ const removableSetRef = defineModel<Set<T>>('removableItems');
13
+
14
+ const expandableSetRef = defineModel<Set<T>>('expandableItems');
15
+ const expandedSetRef = defineModel<Set<T>>('expandedItems');
16
+
17
+ const pinnableSetRef = defineModel<Set<T>>('pinnableItems');
18
+ const pinnedSetRef = defineModel<Set<T>>('pinnedItems');
19
+
20
+ const toggableSetRef = defineModel<Set<T>>('toggableItems');
21
+ const toggledSetRef = defineModel<Set<T>>('toggledItems');
22
+
23
+ const props = withDefaults(
24
+ defineProps<{
25
+ itemClass?: string | string[] | ((item: T, index: number) => string | string[]);
26
+ activeItems?: Set<T>;
27
+
28
+ enableDragging?: boolean;
29
+ getItemKey?: (item: T) => K;
30
+ onDragEnd?: (oldIndex: number, newIndex: number) => void | boolean;
31
+ onSort?: (oldIndex: number, newIndex: number) => void | boolean;
32
+
33
+ enableExpanding?: boolean;
34
+ onExpand?: (item: T, index: number) => void | boolean;
35
+
36
+ enableRemoving?: boolean;
37
+ onRemove?: (item: T, index: number) => void | boolean;
38
+
39
+ enableToggling?: boolean;
40
+ onToggle?: (item: T, index: number) => void | boolean;
41
+
42
+ enablePinning?: boolean;
43
+ onPin?: (item: T, index: number) => void | boolean;
44
+ }>(), {
45
+ itemClass: undefined,
46
+ activeItems: undefined,
47
+
48
+ enableDragging: undefined,
49
+ enableRemoving: undefined,
50
+ enableExpanding: undefined,
51
+ enableToggling: undefined,
52
+ enablePinning: undefined,
53
+
54
+ getItemKey: undefined,
55
+ onDragEnd: undefined,
56
+ onSort: undefined,
57
+ onRemove: undefined,
58
+ onExpand: undefined,
59
+ onToggle: undefined,
60
+ onPin: undefined,
61
+ },
62
+ );
63
+
64
+ const emits = defineEmits<{
65
+ (e: 'itemClick', item: T): void;
66
+ }>();
67
+
68
+ const slots = defineSlots<{
69
+ ['item-title']: (props: { item: T; index: number }) => unknown;
70
+ ['item-content']?: (props: { item: T; index: number }) => unknown;
71
+ }>();
72
+
73
+ const dndSortingEnabled = computed((): boolean => {
74
+ return props.enableDragging !== false && !isNil(props.getItemKey);
75
+ });
76
+
77
+ const pinnedItemsRef = computed(() => itemsRef.value.filter(isPinned));
78
+ const hasPinnedItems = computed(() => pinnedItemsRef.value.length > 0);
79
+
80
+ const unpinnedItemsRef = computed(() => itemsRef.value.filter((item) => !isPinned(item)));
81
+ const hasUnpinnedItems = computed(() => unpinnedItemsRef.value.length > 0);
82
+
83
+ const domProjectionItemsRef = shallowRef<undefined | T[]>();
84
+ const pinnedContainerRef = shallowRef<HTMLElement>();
85
+ const unpinnedContainerRef = shallowRef<HTMLElement>();
86
+
87
+ // version fix problem with sync between data and rendered values when items have been changed
88
+ const versionRef = computed<number>((oldVersion) => {
89
+ const currentVersion = shallowHash(...itemsRef.value);
90
+
91
+ if (domProjectionItemsRef.value === undefined) return oldVersion ?? currentVersion;
92
+
93
+ const lastSortedVersion = shallowHash(...domProjectionItemsRef.value);
94
+
95
+ if (currentVersion === lastSortedVersion) return oldVersion ?? currentVersion;
96
+
97
+ return oldVersion !== currentVersion ? currentVersion : lastSortedVersion;
98
+ });
99
+
100
+ createSortable(hasPinnedItems, pinnedContainerRef, pinnedItemsRef, () => 0);
101
+ createSortable(hasUnpinnedItems, unpinnedContainerRef, unpinnedItemsRef, () => pinnedItemsRef.value.length);
102
+
103
+ function createSortable(toggler: ShallowRef<boolean>, elRef: ShallowRef<undefined | HTMLElement>, itemsRef: ShallowRef<T[]>, getOffset: () => number) {
104
+ const sortable = useSortable(elRef, itemsRef, {
105
+ handle: `[data-draggable="true"]`,
106
+ animation: 150,
107
+ forceFallback: true,
108
+ fallbackOnBody: true,
109
+ scrollSensitivity: 80,
110
+ forceAutoScrollFallback: true,
111
+ onUpdate: (evt: SortableEvent) => {
112
+ if (evt.oldIndex == null || evt.newIndex == null) {
113
+ throw new Error('Sortable event has no index');
114
+ }
115
+ if (props.onDragEnd?.(evt.oldIndex, evt.newIndex) !== false) {
116
+ moveItems(getOffset() + evt.oldIndex, getOffset() + evt.newIndex, true);
117
+ }
118
+ },
119
+ });
120
+ watch(toggler, (on) => on ? sortable.start() : sortable.stop());
121
+
122
+ return sortable;
123
+ }
124
+
125
+ function moveItems(oldIndex: number, newIndex: number, afterUpdateDom: boolean) {
126
+ if (oldIndex === newIndex) return;
127
+
128
+ if (afterUpdateDom) {
129
+ domProjectionItemsRef.value = moveElements(itemsRef.value.slice(), oldIndex, newIndex);
130
+ }
131
+
132
+ const preventDefault = props.onSort?.(oldIndex, newIndex) === false;
133
+
134
+ if (!preventDefault) {
135
+ moveElements(itemsRef.value, oldIndex, newIndex);
136
+ optionalUpdateRef(itemsRef);
137
+ }
138
+ }
139
+
140
+ function isActive(item: T): boolean {
141
+ return props.activeItems?.has(item) ?? false;
142
+ }
143
+
144
+ function isDraggable(item: T): boolean {
145
+ if (props.enableDragging === false) return false;
146
+ return (draggableSetRef.value?.has(item) ?? true);
147
+ }
148
+
149
+ function isToggable(item: T): boolean {
150
+ if (props.enableToggling === false) return false;
151
+ return !isNil(toggledSetRef.value) && (toggableSetRef.value?.has(item) ?? true);
152
+ }
153
+
154
+ function isToggled(item: T): boolean {
155
+ return toggledSetRef.value?.has(item) ?? false;
156
+ }
157
+
158
+ function isPinnable(item: T): boolean {
159
+ if (props.enablePinning === false) return false;
160
+ return !isNil(pinnedSetRef.value) && (pinnableSetRef.value?.has(item) ?? true);
161
+ }
162
+
163
+ function isPinned(item: T): boolean {
164
+ return pinnedSetRef.value?.has(item) ?? false;
165
+ }
166
+
167
+ function isExpandable(item: T): boolean {
168
+ if (props.enableExpanding === false) return false;
169
+ return !isNil(expandedSetRef.value) && (expandableSetRef.value?.has(item) ?? true);
170
+ }
171
+
172
+ function isExpanded(item: T): boolean {
173
+ return expandedSetRef.value?.has(item) ?? false;
174
+ }
175
+
176
+ function isRemovable(item: T): boolean {
177
+ if (props.enableRemoving === false) return false;
178
+ if (removableSetRef.value?.has(item) === false) return false;
179
+ return props.enableRemoving === true || typeof props.onRemove === 'function';
180
+ }
181
+
182
+ function handleExpand(item: T, index: number) {
183
+ if (props.onExpand?.(item, index) === false || isNil(expandedSetRef.value)) return;
184
+
185
+ const expanded = expandedSetRef.value;
186
+ if (expanded.has(item)) expanded.delete(item);
187
+ else expanded.add(item);
188
+ optionalUpdateRef(expandedSetRef);
189
+ }
190
+
191
+ function handleToggle(item: T, index: number) {
192
+ if (props.onToggle?.(item, index) === false || isNil(toggledSetRef.value)) return;
193
+
194
+ const toggled = toggledSetRef.value;
195
+ if (toggled.has(item)) toggled.delete(item);
196
+ else toggled.add(item);
197
+ optionalUpdateRef(toggledSetRef);
198
+ }
199
+
200
+ function handlePin(item: T, oldIndex: number) {
201
+ if (oldIndex === -1) {
202
+ throw new Error('Pinnable item not found');
203
+ }
204
+
205
+ if (props.onPin?.(item, oldIndex) === false || isNil(pinnedSetRef.value)) return;
206
+
207
+ const pinned = pinnedSetRef.value;
208
+ const alreadyPinned = pinned.has(item);
209
+ if (alreadyPinned) pinned.delete(item);
210
+ else pinned.add(item);
211
+ optionalUpdateRef(pinnedSetRef);
212
+ moveItems(oldIndex, pinned.size + (alreadyPinned ? 0 : -1), false);
213
+ }
214
+
215
+ function handleRemove(item: T, index: number) {
216
+ if (props.onRemove?.(item, index) !== false) {
217
+ itemsRef.value.splice(index, 1);
218
+ optionalUpdateRef(itemsRef);
219
+
220
+ if (pinnedSetRef.value?.has(item)) {
221
+ pinnedSetRef.value.delete(item);
222
+ optionalUpdateRef(pinnedSetRef);
223
+ }
224
+
225
+ if (toggledSetRef.value?.has(item)) {
226
+ toggledSetRef.value.delete(item);
227
+ optionalUpdateRef(toggledSetRef);
228
+ }
229
+ }
230
+ }
231
+
232
+ // version fix problem with sync between data and rendered values
233
+ const getKey = (item: T, index: number) => {
234
+ if (isNil(props.getItemKey)) return `${versionRef.value}-${index}`;
235
+ return `${versionRef.value}-${props.getItemKey(item)}`;
236
+ };
237
+ const pinnedKeysRef = computed(() => pinnedItemsRef.value.map(getKey));
238
+ const unpinnedKeysRef = computed(() => unpinnedItemsRef.value.map(getKey));
239
+
240
+ const getItemClass = (item: T, index: number): null | string | string[] => {
241
+ if (typeof props.itemClass === 'function') {
242
+ return props.itemClass(item, index);
243
+ }
244
+ return props.itemClass ?? null;
245
+ };
246
+
247
+ </script>
248
+
249
+ <template>
250
+ <div :class="$style.root">
251
+ <div ref="pinnedContainerRef" :class="$style.list">
252
+ <PlElementListItem
253
+ v-for="(pinnedItem, pinnedIndex) in pinnedItemsRef" :key="pinnedKeysRef[pinnedIndex]"
254
+ :class="[$style.item, getItemClass(pinnedItem, pinnedIndex)]"
255
+
256
+ :index="pinnedIndex"
257
+ :item="pinnedItem"
258
+ :showDragHandle="dndSortingEnabled"
259
+ :isActive="isActive(pinnedItem)"
260
+ :isDraggable="isDraggable(pinnedItem)"
261
+ :isRemovable="isRemovable(pinnedItem)"
262
+ :isToggable="isToggable(pinnedItem)"
263
+ :isToggled="isToggled(pinnedItem)"
264
+ :isPinnable="isPinnable(pinnedItem)"
265
+ :isPinned="isPinned(pinnedItem)"
266
+ :isExpandable="isExpandable(pinnedItem)"
267
+ :isExpanded="isExpanded(pinnedItem)"
268
+
269
+ @click="emits('itemClick', pinnedItem)"
270
+ @remove="handleRemove"
271
+ @toggle="handleToggle"
272
+ @pin="handlePin"
273
+ @expand="handleExpand"
274
+ >
275
+ <template #title="{ item, index }">
276
+ <slot :index="index" :item="item" name="item-title" />
277
+ </template>
278
+ <template v-if="slots['item-content']" #content="{ item, index }">
279
+ <slot :index="index" :item="item" name="item-content" />
280
+ </template>
281
+ </PlElementListItem>
282
+ </div>
283
+ <div v-if="hasUnpinnedItems" ref="unpinnedContainerRef" :class="$style.list">
284
+ <PlElementListItem
285
+ v-for="(unpinnedItem, unpinnedIndex) in unpinnedItemsRef" :key="unpinnedKeysRef[unpinnedIndex]"
286
+ :class="[$style.item, getItemClass(unpinnedItem, unpinnedIndex)]"
287
+
288
+ :index="unpinnedIndex + (pinnedSetRef?.size ?? 0)"
289
+ :item="unpinnedItem"
290
+ :showDragHandle="dndSortingEnabled"
291
+ :isActive="isActive(unpinnedItem)"
292
+ :isDraggable="isDraggable(unpinnedItem)"
293
+ :isRemovable="isRemovable(unpinnedItem)"
294
+ :isToggable="isToggable(unpinnedItem)"
295
+ :isToggled="isToggled(unpinnedItem)"
296
+ :isPinnable="isPinnable(unpinnedItem)"
297
+ :isPinned="isPinned(unpinnedItem)"
298
+ :isExpandable="isExpandable(unpinnedItem)"
299
+ :isExpanded="isExpanded(unpinnedItem)"
300
+
301
+ @click="emits('itemClick', unpinnedItem)"
302
+ @remove="handleRemove"
303
+ @toggle="handleToggle"
304
+ @pin="handlePin"
305
+ @expand="handleExpand"
306
+ >
307
+ <template #title="{ item, index }">
308
+ <slot :index="index" :item="item" name="item-title" />
309
+ </template>
310
+ <template v-if="slots['item-content']" #content="{ item, index }">
311
+ <slot :index="index" :item="item" name="item-content" />
312
+ </template>
313
+ </PlElementListItem>
314
+ </div>
315
+ </div>
316
+ </template>
317
+
318
+ <style module>
319
+ .root, .list {
320
+ display: flex;
321
+ flex-direction: column;
322
+ gap: 8px;
323
+ min-width: 180px;
324
+ }
325
+
326
+ .item {
327
+ width: 100%;
328
+ }
329
+
330
+ :global(.sortable-ghost) {
331
+ visibility: hidden;
332
+ }
333
+ :global(.sortable-drag) {
334
+ opacity: 1;
335
+ }
336
+ </style>
@@ -0,0 +1,239 @@
1
+ <script generic="T extends unknown = unknown" lang="ts" setup>
2
+ import { computed } from 'vue';
3
+ import { PlIcon16 } from '../PlIcon16';
4
+ import { PlIcon24 } from '../PlIcon24';
5
+
6
+ const props = defineProps<{
7
+ item: T;
8
+ index: number;
9
+ showDragHandle: boolean;
10
+ isActive: boolean;
11
+ isDraggable: boolean;
12
+ isRemovable: boolean;
13
+ isExpandable: boolean;
14
+ isExpanded: boolean;
15
+ isToggable: boolean;
16
+ isToggled: boolean;
17
+ isPinnable: boolean;
18
+ isPinned: boolean;
19
+ }>();
20
+
21
+ const slots = defineSlots<{
22
+ title: (props: { item: T; index: number }) => unknown;
23
+ content?: (props: { item: T; index: number }) => unknown;
24
+ }>();
25
+ const hasContentSlot = computed(() => slots['content'] !== undefined);
26
+
27
+ const emit = defineEmits<{
28
+ (e: 'expand', item: T, index: number): void;
29
+ (e: 'toggle', item: T, index: number): void;
30
+ (e: 'pin', item: T, index: number): void;
31
+ (e: 'remove', item: T, index: number): void;
32
+ }>();
33
+ </script>
34
+
35
+ <template>
36
+ <div
37
+ :class="[$style.root, {
38
+ [$style.active]: props.isActive,
39
+ [$style.pinned]: props.isPinned,
40
+ [$style.disabled]: props.isToggled,
41
+ }]"
42
+ >
43
+ <div
44
+ :class="[$style.head, {
45
+ [$style.clickable]: hasContentSlot,
46
+ }]"
47
+ @click="isExpandable && emit('expand', props.item, props.index)"
48
+ >
49
+ <div
50
+ v-if="props.showDragHandle"
51
+ :class="[$style.action, $style.draggable, { [$style.disable]: !props.isDraggable } ]"
52
+ :data-draggable="props.isDraggable"
53
+ >
54
+ <PlIcon16 name="drag-dots" />
55
+ </div>
56
+ <PlIcon16 v-if="isExpandable" :class="[$style.contentChevron, { [$style.opened]: props.isExpanded }]" name="chevron-down" />
57
+
58
+ <div :class="$style.title">
59
+ <slot name="title" :item="props.item" :index="props.index" />
60
+ </div>
61
+
62
+ <div :class="[$style.actions, $style.showOnHover]">
63
+ <div
64
+ v-if="props.isToggable"
65
+ :class="[$style.action, $style.clickable, { [$style.disable]: !props.isToggable }]"
66
+ @click.stop="emit('toggle', props.item, props.index)"
67
+ >
68
+ <PlIcon24 :name="props.isToggled === true ? 'view-hide' : 'view-show'" size="16" />
69
+ </div>
70
+ <div
71
+ v-if="props.isPinnable"
72
+ :class="[$style.action, $style.clickable, {
73
+ [$style.disable]: !props.isPinnable,
74
+ [$style.activated]: props.isPinned,
75
+ }]"
76
+ @click.stop="emit('pin', props.item, props.index)"
77
+ >
78
+ <PlIcon24 name="pin" size="16" />
79
+ </div>
80
+ <div
81
+ v-if="props.isRemovable"
82
+ :class="[$style.action, $style.clickable]"
83
+ @click.stop="emit('remove', props.item, props.index)"
84
+ >
85
+ <PlIcon16 name="close" />
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <div v-if="hasContentSlot && props.isExpanded" :class="$style.body">
90
+ <slot name="content" :item="props.item" :index="props.index" />
91
+ </div>
92
+ </div>
93
+ </template>
94
+
95
+ <style module>
96
+ @use '../../assets/variables.scss' as *;
97
+
98
+ .root {
99
+ --background: rgba(255, 255, 255, 0.8);
100
+ --border-color: var(--color-div-grey);
101
+ --head-background: unset;
102
+ --box-shadow: none;
103
+ --box-shadow-active: 0 0 0 4px color-mix(in srgb, var(--border-color-focus) 50%, transparent);
104
+
105
+ &:global(.sortable-drag),
106
+ &:global(.sortable-chosen) {
107
+ --head-background: var(--gradient-light-lime);
108
+ --border-color: var(--border-color-focus);
109
+ --box-shadow: var(--box-shadow-active)
110
+ }
111
+ }
112
+ .root {
113
+ display: flex;
114
+ flex-direction: column;
115
+ justify-content: center;
116
+ border-radius: var(--border-radius);
117
+ border: 1px solid var(--border-color);
118
+ background-color: var(--background);
119
+ transition: box-shadow 0.15s;
120
+ box-shadow: var(--box-shadow);;
121
+ overflow: hidden;
122
+
123
+ &:hover {
124
+ --border-color: var(--border-color-focus);
125
+ }
126
+
127
+ &.disabled {
128
+ opacity: 0.6;
129
+ filter: grayscale(1);
130
+ }
131
+
132
+ &.pinned {
133
+ --background: var(--bg-base-light);
134
+ }
135
+
136
+ &.active {
137
+ --border-color: var(--border-color-focus);
138
+ --head-background: var(--btn-accent-positive-500);
139
+ }
140
+ }
141
+
142
+ .head {
143
+ position: relative;
144
+ display: flex;
145
+ align-items: center;
146
+ padding: 8px;
147
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
148
+ background: var(--head-background);
149
+
150
+ &:hover, &.opened {
151
+ --head-background: var(--gradient-light-lime);
152
+ }
153
+ }
154
+
155
+ .contentChevron {
156
+ display: block;
157
+ width: 16px;
158
+ height: 16px;
159
+ margin-right: 4px;
160
+ transform: rotate(-90deg);
161
+ transition: transform 0.15s;
162
+
163
+ &.opened {
164
+ transform: rotate(0deg);
165
+ }
166
+ }
167
+
168
+ .title {
169
+ display: flex;
170
+ flex-direction: row;
171
+ gap: 8px;
172
+ max-width: calc(100% - 50px);
173
+ overflow: hidden;
174
+ text-overflow: ellipsis;
175
+ }
176
+
177
+ .body {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 8px;
181
+ padding: 24px;
182
+ border-radius: 0 0 var(--border-radius) var(--border-radius);
183
+ }
184
+
185
+ .actions {
186
+ position: absolute;
187
+ top: 8px;
188
+ right: 8px;
189
+ display: flex;
190
+ align-items: center;
191
+ background-color: var(--background);
192
+ border-radius: var(--border-radius);
193
+ }
194
+
195
+ .action {
196
+ width: 24px;
197
+ height: 24px;
198
+ padding: 4px; /* use padding instead of gap on parent, for better accessibility */
199
+ opacity: 0.6;
200
+ border-radius: var(--border-radius);
201
+ transition: all 0.15s;
202
+
203
+ svg {
204
+ width: 16px;
205
+ height: 16px;
206
+ }
207
+
208
+ &:hover {
209
+ opacity: 1;
210
+ background-color: var(--bg-elevated-02);
211
+ }
212
+
213
+ &.activated {
214
+ opacity: 0.8;
215
+ }
216
+
217
+ &.disable {
218
+ cursor: not-allowed;
219
+ opacity: 0.4;
220
+ }
221
+ }
222
+
223
+ .clickable {
224
+ cursor: pointer;
225
+ }
226
+
227
+ .draggable {
228
+ cursor: grab;
229
+ }
230
+
231
+ .showOnHover {
232
+ opacity: 0;
233
+ transition: opacity 0.15s;
234
+ }
235
+
236
+ .root:hover .showOnHover {
237
+ opacity: 1;
238
+ }
239
+ </style>
@@ -0,0 +1,92 @@
1
+ PlElementList.vue
2
+ =================
3
+
4
+ ## 1. GENERICS
5
+
6
+ • **`T`** – Runtime type of a single list element
7
+ • **`K`** – Type returned by `getItemKey` (typically `string` or `number`)
8
+
9
+
10
+ ## 2. TWO-WAY BINDINGS (`v-model`)
11
+
12
+ | Model name | Type | Purpose / Behaviour |
13
+ |-------------------|----------|--------------------------------------------------------|
14
+ | `items`* | `T[]` | Source array; the list you render & mutate |
15
+ | `draggableItems` | `Set<T>` | Restricts which items may be dragged |
16
+ | `removableItems` | `Set<T>` | Restricts which items may be removed |
17
+ | `expandableItems` | `Set<T>` | Restricts which items may be expanded |
18
+ | `expandedItems` | `Set<T>` | Tracks the expand/collapse state per item |
19
+ | `pinnableItems` | `Set<T>` | Restricts which items may be pinned |
20
+ | `pinnedItems` | `Set<T>` | Tracks the *current* pinned elements |
21
+ | `toggableItems` | `Set<T>` | Restricts which items may be toggled (show/hide) |
22
+ | `toggledItems` | `Set<T>` | Tracks the visibility toggle state per item |
23
+
24
+ (*required)
25
+
26
+ ## 3. PROPS
27
+
28
+ | Prop | Type | Default | Notes |
29
+ |------------------|-----------------------------------------------------------|-------------|-------------------------------------------------|
30
+ | `getItemKey` | `(item:T) ⇒ K` | — | Stable key for `v-for` & SortableJS |
31
+ | `itemClass` | `string \| string[] \| ((item:T, index:number) ⇒ string \| string[])` | — | CSS classes for individual items |
32
+ | `activeItems` | `Set<T>` | — | Set of currently active items |
33
+ | **Sorting** | | | |
34
+ | `enableDragging` | `boolean` | `undefined` | Master switch for drag-and-drop |
35
+ | `onDragEnd` | `(oldIdx:number, newIdx:number) ⇒ void\|bool` | — | Fired by SortableJS; return **false** to ignore |
36
+ | `onSort` | `(oldIdx:number, newIdx:number) ⇒ void\|bool` | — | Return **false** to cancel applying new order |
37
+ | **Removing** | | | |
38
+ | `enableRemoving` | `boolean` | `undefined` | `true` ⇒ always show remove, `false` ⇒ never |
39
+ | `onRemove` | `(item:T, index:number) ⇒ void\|bool` | — | Return **false** to veto |
40
+ | **Expanding** | | | |
41
+ | `enableExpanding`| `boolean` | `undefined` | Master switch for expand/collapse |
42
+ | `onExpand` | `(item:T, index:number) ⇒ void\|bool` | — | Return **false** to veto |
43
+ | **Toggling** | | | |
44
+ | `enableToggling` | `boolean` | `undefined` | Master switch for visibility toggle |
45
+ | `onToggle` | `(item:T, index:number) ⇒ void\|bool` | — | Return **false** to veto |
46
+ | **Pinning** | | | |
47
+ | `enablePinning` | `boolean` | `undefined` | Master switch for pinning |
48
+ | `onPin` | `(item:T, index:number) ⇒ void\|bool` | — | Return **false** to veto |
49
+
50
+
51
+ ## 4. SLOTS
52
+
53
+ - **`item-title`** – **required**. Receives `{ item, index }`.
54
+ - **`item-content`** – optional. Same slot props; rendered only when the item
55
+ is in the “opened” state (`toggleContent` event).
56
+
57
+
58
+ ## 5. EVENTS
59
+
60
+ | Event | Payload | Description |
61
+ |------------|---------------------|------------------------------------------------|
62
+ | `itemClick`| `(item: T)` | Emitted when an item is clicked |
63
+
64
+ Additionally, these events bubble up from `<PlElementListItem>`:
65
+ - `remove` – `(item: T, index: number)`
66
+ - `toggle` – `(item: T, index: number)`
67
+ - `pin` – `(item: T, index: number)`
68
+ - `expand` – `(item: T, index: number)`
69
+
70
+
71
+ ## 6. EXAMPLE USAGE
72
+
73
+ ```vue
74
+ <PlElementList
75
+ v-model:items="elements"
76
+ v-model:pinnedItems="pinned"
77
+ v-model:expandedItems="expanded"
78
+ v-model:toggledItems="hidden"
79
+ :getItemKey="el => el.id"
80
+ :onSort="(oldIdx, newIdx) => api.saveOrder(oldIdx, newIdx)"
81
+ :activeItems="activeSet"
82
+ :itemClass="(item, index) => ({ 'special-item': item.isSpecial })"
83
+ @itemClick="handleItemClick"
84
+ >
85
+ <template #item-title="{ item }">
86
+ {{ item.name }}
87
+ </template>
88
+ <template #item-content="{ item }">
89
+ <pre>{{ JSON.stringify(item.meta, null, 2) }}</pre>
90
+ </template>
91
+ </PlElementList>
92
+ ```
@@ -0,0 +1 @@
1
+ export { default as PlElementList } from './PlElementList.vue';
@@ -0,0 +1,17 @@
1
+ import { isRef, isShallow } from 'vue';
2
+ import { shallowClone } from '@milaboratories/helpers';
3
+
4
+ export const moveElements = <T>(array: T[], from: number, to: number): T[] => {
5
+ if (to >= 0 && to < array.length) {
6
+ const element = array.splice(from, 1)[0];
7
+ array.splice(to, 0, element);
8
+ }
9
+
10
+ return array;
11
+ };
12
+
13
+ export function optionalUpdateRef<T>(ref: T) {
14
+ if (isRef(ref) && isShallow(ref)) {
15
+ ref.value = shallowClone(ref.value);
16
+ }
17
+ }
package/src/index.ts CHANGED
@@ -70,6 +70,7 @@ export * from './components/PlLoaderCircular';
70
70
  export * from './components/PlSplash';
71
71
  export * from './components/PlProgressCell';
72
72
  export * from './components/PlAutocomplete';
73
+ export * from './components/PlElementList';
73
74
 
74
75
  export * from './components/PlFileDialog';
75
76
  export * from './components/PlFileInput';