@flux-ui/components 3.0.0-next.51 → 3.0.0-next.53
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/component/FluxKanban.vue.d.ts +25 -0
- package/dist/component/FluxKanbanCard.vue.d.ts +28 -0
- package/dist/component/FluxKanbanColumn.vue.d.ts +28 -0
- package/dist/component/index.d.ts +3 -0
- package/dist/composable/private/useKanban.d.ts +7 -0
- package/dist/data/di.d.ts +20 -0
- package/dist/index.css +84 -0
- package/dist/index.js +238 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/component/FluxKanban.vue +25 -0
- package/src/component/FluxKanbanCard.vue +105 -0
- package/src/component/FluxKanbanColumn.vue +84 -0
- package/src/component/index.ts +3 -0
- package/src/composable/private/useKanban.ts +68 -0
- package/src/css/component/FluxKanban.module.scss +7 -0
- package/src/css/component/FluxKanbanCard.module.scss +35 -0
- package/src/css/component/FluxKanbanColumn.module.scss +49 -0
- package/src/data/di.ts +21 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flux-ui/components",
|
|
3
3
|
"description": "A set of opiniated UI components.",
|
|
4
|
-
"version": "3.0.0-next.
|
|
4
|
+
"version": "3.0.0-next.53",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://github.com/sponsors/basmilius",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@basmilius/common": "^3.19.0",
|
|
51
51
|
"@basmilius/utils": "^3.19.0",
|
|
52
|
-
"@flux-ui/internals": "3.0.0-next.
|
|
53
|
-
"@flux-ui/types": "3.0.0-next.
|
|
52
|
+
"@flux-ui/internals": "3.0.0-next.53",
|
|
53
|
+
"@flux-ui/types": "3.0.0-next.53",
|
|
54
54
|
"@fortawesome/fontawesome-common-types": "^7.2.0",
|
|
55
55
|
"clsx": "^2.1.1",
|
|
56
56
|
"imask": "^7.6.1",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="$style.kanban">
|
|
3
|
+
<slot/>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script
|
|
8
|
+
lang="ts"
|
|
9
|
+
setup>
|
|
10
|
+
import type { FluxKanbanMoveEvent } from '@flux-ui/types';
|
|
11
|
+
import { provide } from 'vue';
|
|
12
|
+
import { FluxKanbanInjectionKey } from '$flux/data/di';
|
|
13
|
+
import { useKanban } from '$flux/composable/private/useKanban';
|
|
14
|
+
import $style from '$flux/css/component/FluxKanban.module.scss';
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
move: [FluxKanbanMoveEvent];
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
defineSlots<{
|
|
21
|
+
default?(): any;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
provide(FluxKanbanInjectionKey, useKanban(event => emit('move', event)));
|
|
25
|
+
</script>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="root"
|
|
4
|
+
data-kanban-card
|
|
5
|
+
:class="[
|
|
6
|
+
$style.kanbanCard,
|
|
7
|
+
isDragging && $style.isDragging,
|
|
8
|
+
isDropBefore && $style.isDropBefore
|
|
9
|
+
]"
|
|
10
|
+
draggable="true"
|
|
11
|
+
@dragstart="onDragStart"
|
|
12
|
+
@dragend="onDragEnd"
|
|
13
|
+
@dragover.prevent.stop="onDragOver">
|
|
14
|
+
<slot/>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script
|
|
19
|
+
lang="ts"
|
|
20
|
+
setup>
|
|
21
|
+
import { computed, inject, onMounted, onUnmounted, unref, useTemplateRef } from 'vue';
|
|
22
|
+
import { FluxKanbanInjectionKey } from '$flux/data/di';
|
|
23
|
+
import $style from '$flux/css/component/FluxKanbanCard.module.scss';
|
|
24
|
+
|
|
25
|
+
const {cardId, columnId} = defineProps<{
|
|
26
|
+
readonly cardId: string | number;
|
|
27
|
+
readonly columnId: string | number;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
defineSlots<{
|
|
31
|
+
default?(): any;
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
const kanban = inject(FluxKanbanInjectionKey)!;
|
|
35
|
+
const root = useTemplateRef('root');
|
|
36
|
+
|
|
37
|
+
const isDragging = computed(() => unref(kanban.dragState)?.cardId === cardId);
|
|
38
|
+
|
|
39
|
+
const isDropBefore = computed(() => {
|
|
40
|
+
const state = unref(kanban.dragState);
|
|
41
|
+
|
|
42
|
+
if (!state || state.dropColumnId === null || state.beforeCardId === null) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return state.beforeCardId === cardId && state.cardId !== cardId;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
if (root.value) {
|
|
51
|
+
kanban.registerCard(root.value, cardId);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
onUnmounted(() => {
|
|
56
|
+
if (root.value) {
|
|
57
|
+
kanban.unregisterCard(root.value);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function onDragStart(evt: DragEvent): void {
|
|
62
|
+
kanban.startDrag(cardId, columnId);
|
|
63
|
+
|
|
64
|
+
if (evt.dataTransfer) {
|
|
65
|
+
evt.dataTransfer.effectAllowed = 'move';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onDragEnd(): void {
|
|
70
|
+
kanban.endDrag();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onDragOver(evt: DragEvent): void {
|
|
74
|
+
const state = unref(kanban.dragState);
|
|
75
|
+
|
|
76
|
+
if (!state) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cardEl = evt.currentTarget as Element;
|
|
81
|
+
const rect = cardEl.getBoundingClientRect();
|
|
82
|
+
|
|
83
|
+
if (evt.clientY < rect.top + rect.height / 2) {
|
|
84
|
+
// Drop before this card
|
|
85
|
+
kanban.updateDropTarget(columnId, cardId);
|
|
86
|
+
} else {
|
|
87
|
+
// Drop after this card = before the next sibling card
|
|
88
|
+
let next = cardEl.nextElementSibling;
|
|
89
|
+
|
|
90
|
+
while (next) {
|
|
91
|
+
const info = kanban.getCardInfo(next);
|
|
92
|
+
|
|
93
|
+
if (info) {
|
|
94
|
+
kanban.updateDropTarget(columnId, info.cardId);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
next = next.nextElementSibling;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// No next sibling → append to end of column
|
|
102
|
+
kanban.updateDropTarget(columnId, null);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
data-kanban-column
|
|
4
|
+
:class="[$style.kanbanColumn, isOver && $style.isOver]">
|
|
5
|
+
<div :class="$style.kanbanColumnHeader">
|
|
6
|
+
<slot name="header">
|
|
7
|
+
<span :class="$style.kanbanColumnLabel">{{ label }}</span>
|
|
8
|
+
</slot>
|
|
9
|
+
|
|
10
|
+
<slot name="actions"/>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
:class="$style.kanbanColumnBody"
|
|
15
|
+
@dragenter="onDragEnter"
|
|
16
|
+
@dragleave="onDragLeave"
|
|
17
|
+
@dragover.prevent="onDragOver"
|
|
18
|
+
@drop.prevent="onDrop">
|
|
19
|
+
<slot/>
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
v-if="isDropEnd"
|
|
23
|
+
:class="$style.kanbanDropIndicator"/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script
|
|
29
|
+
lang="ts"
|
|
30
|
+
setup>
|
|
31
|
+
import { computed, inject, ref, unref } from 'vue';
|
|
32
|
+
import { FluxKanbanInjectionKey } from '$flux/data/di';
|
|
33
|
+
import $style from '$flux/css/component/FluxKanbanColumn.module.scss';
|
|
34
|
+
|
|
35
|
+
const {columnId, label} = defineProps<{
|
|
36
|
+
readonly columnId: string | number;
|
|
37
|
+
readonly label: string;
|
|
38
|
+
}>();
|
|
39
|
+
|
|
40
|
+
defineSlots<{
|
|
41
|
+
default?(): any;
|
|
42
|
+
header?(): any;
|
|
43
|
+
actions?(): any;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
const kanban = inject(FluxKanbanInjectionKey)!;
|
|
47
|
+
|
|
48
|
+
let dragEnterCount = 0;
|
|
49
|
+
const isOver = ref(false);
|
|
50
|
+
|
|
51
|
+
const isDropEnd = computed(() => {
|
|
52
|
+
const state = unref(kanban.dragState);
|
|
53
|
+
return state !== null && state.dropColumnId === columnId && state.beforeCardId === null;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function onDragEnter(): void {
|
|
57
|
+
dragEnterCount++;
|
|
58
|
+
isOver.value = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onDragLeave(): void {
|
|
62
|
+
if (--dragEnterCount <= 0) {
|
|
63
|
+
dragEnterCount = 0;
|
|
64
|
+
isOver.value = false;
|
|
65
|
+
kanban.clearDropTarget();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onDragOver(evt: DragEvent): void {
|
|
70
|
+
// Only handle empty-area drops; cards stop propagation on their own dragover
|
|
71
|
+
const target = evt.target as Element;
|
|
72
|
+
const isOverCard = !!target.closest('[data-kanban-card]');
|
|
73
|
+
|
|
74
|
+
if (!isOverCard) {
|
|
75
|
+
kanban.updateDropTarget(columnId, null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onDrop(): void {
|
|
80
|
+
dragEnterCount = 0;
|
|
81
|
+
isOver.value = false;
|
|
82
|
+
kanban.commitDrop();
|
|
83
|
+
}
|
|
84
|
+
</script>
|
package/src/component/index.ts
CHANGED
|
@@ -80,6 +80,9 @@ export { default as FluxGridColumn } from './FluxGridColumn.vue';
|
|
|
80
80
|
export { default as FluxGridPattern } from './FluxGridPattern.vue';
|
|
81
81
|
export { default as FluxIcon } from './FluxIcon.vue';
|
|
82
82
|
export { default as FluxInfo } from './FluxInfo.vue';
|
|
83
|
+
export { default as FluxKanban } from './FluxKanban.vue';
|
|
84
|
+
export { default as FluxKanbanCard } from './FluxKanbanCard.vue';
|
|
85
|
+
export { default as FluxKanbanColumn } from './FluxKanbanColumn.vue';
|
|
83
86
|
export { default as FluxInfoStack } from './FluxInfoStack.vue';
|
|
84
87
|
export { default as FluxItem } from './FluxItem.vue';
|
|
85
88
|
export { default as FluxItemActions } from './FluxItemActions.vue';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { FluxKanbanMoveEvent } from '@flux-ui/types';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import type { FluxKanbanDragState, FluxKanbanInjection } from '$flux/data/di';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal composable for managing kanban drag-and-drop state.
|
|
7
|
+
* Provides card registration, drag tracking, and drop target management.
|
|
8
|
+
*/
|
|
9
|
+
export function useKanban(onMove: (event: FluxKanbanMoveEvent) => void): FluxKanbanInjection {
|
|
10
|
+
const dragState = ref<FluxKanbanDragState | null>(null);
|
|
11
|
+
const cardRegistry = new WeakMap<Element, { readonly cardId: string | number }>();
|
|
12
|
+
|
|
13
|
+
function registerCard(element: Element, cardId: string | number): void {
|
|
14
|
+
cardRegistry.set(element, {cardId});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function unregisterCard(element: Element): void {
|
|
18
|
+
cardRegistry.delete(element);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCardInfo(element: Element): { readonly cardId: string | number } | undefined {
|
|
22
|
+
return cardRegistry.get(element);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function startDrag(cardId: string | number, fromColumnId: string | number): void {
|
|
26
|
+
dragState.value = {cardId, fromColumnId, dropColumnId: null, beforeCardId: null};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function endDrag(): void {
|
|
30
|
+
dragState.value = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function updateDropTarget(columnId: string | number, beforeCardId: string | number | null): void {
|
|
34
|
+
if (!dragState.value) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dragState.value = {...dragState.value, dropColumnId: columnId, beforeCardId};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clearDropTarget(): void {
|
|
42
|
+
if (!dragState.value) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
dragState.value = {...dragState.value, dropColumnId: null, beforeCardId: null};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function commitDrop(): void {
|
|
50
|
+
const state = dragState.value;
|
|
51
|
+
|
|
52
|
+
if (!state || state.dropColumnId === null) {
|
|
53
|
+
dragState.value = null;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onMove({
|
|
58
|
+
cardId: state.cardId,
|
|
59
|
+
fromColumnId: state.fromColumnId,
|
|
60
|
+
toColumnId: state.dropColumnId,
|
|
61
|
+
beforeCardId: state.beforeCardId ?? undefined
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
dragState.value = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {dragState, registerCard, unregisterCard, getCardInfo, startDrag, endDrag, updateDropTarget, clearDropTarget, commitDrop};
|
|
68
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.kanbanCard {
|
|
2
|
+
position: relative;
|
|
3
|
+
padding: 12px;
|
|
4
|
+
background: var(--gray-25);
|
|
5
|
+
border: 1px solid var(--gray-200);
|
|
6
|
+
border-radius: var(--radius);
|
|
7
|
+
cursor: grab;
|
|
8
|
+
transition: opacity 180ms var(--swift-out), box-shadow 180ms var(--swift-out);
|
|
9
|
+
user-select: none;
|
|
10
|
+
|
|
11
|
+
&:active {
|
|
12
|
+
cursor: grabbing;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
box-shadow: 0 1px 4px rgb(0 0 0 / .08);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.isDragging {
|
|
21
|
+
opacity: .4;
|
|
22
|
+
cursor: grabbing;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.isDropBefore {
|
|
26
|
+
&::before {
|
|
27
|
+
content: '';
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset-inline: 0;
|
|
30
|
+
top: -6px;
|
|
31
|
+
height: 2px;
|
|
32
|
+
border-radius: 999px;
|
|
33
|
+
background: var(--primary-500);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.kanbanColumn {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-flow: column;
|
|
4
|
+
flex-shrink: 0;
|
|
5
|
+
width: 280px;
|
|
6
|
+
background: var(--gray-100);
|
|
7
|
+
border: 1px solid var(--gray-200);
|
|
8
|
+
border-radius: var(--radius);
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.kanbanColumnHeader {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 9px;
|
|
16
|
+
padding: 12px 15px;
|
|
17
|
+
border-bottom: 1px solid var(--gray-200);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.kanbanColumnLabel {
|
|
21
|
+
flex: 1;
|
|
22
|
+
font-size: .8125rem;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
letter-spacing: .01em;
|
|
26
|
+
text-transform: uppercase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.kanbanColumnBody {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-flow: column;
|
|
32
|
+
gap: 9px;
|
|
33
|
+
padding: 9px;
|
|
34
|
+
min-height: 60px;
|
|
35
|
+
flex: 1;
|
|
36
|
+
overflow-y: auto;
|
|
37
|
+
transition: background 180ms var(--swift-out);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.isOver .kanbanColumnBody {
|
|
41
|
+
background: rgb(from var(--primary-500) r g b / .06);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.kanbanDropIndicator {
|
|
45
|
+
height: 2px;
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
background: var(--primary-500);
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
package/src/data/di.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ComponentInternalInstance, InjectionKey, Ref } from 'vue';
|
|
|
3
3
|
|
|
4
4
|
export const FluxAdaptiveGroupInjectionKey: InjectionKey<FluxAdaptiveGroupInjection> = Symbol();
|
|
5
5
|
export const FluxDisabledInjectionKey: InjectionKey<Ref<boolean>> = Symbol();
|
|
6
|
+
export const FluxKanbanInjectionKey: InjectionKey<FluxKanbanInjection> = Symbol();
|
|
6
7
|
export const FluxExpandableGroupInjectionKey: InjectionKey<FluxExpandableGroupInjection> = Symbol();
|
|
7
8
|
export const FluxFlyoutInjectionKey: InjectionKey<FluxFlyoutInjection> = Symbol();
|
|
8
9
|
export const FluxFilterInjectionKey: InjectionKey<FluxFilterInjection> = Symbol();
|
|
@@ -10,6 +11,26 @@ export const FluxFormFieldInjectionKey: InjectionKey<FluxFormFieldInjection> = S
|
|
|
10
11
|
export const FluxTableInjectionKey: InjectionKey<FluxTableInjection> = Symbol();
|
|
11
12
|
export const FluxTooltipInjectionKey: InjectionKey<FluxTooltipInjection> = Symbol();
|
|
12
13
|
|
|
14
|
+
export type FluxKanbanDragState = {
|
|
15
|
+
readonly cardId: string | number;
|
|
16
|
+
readonly fromColumnId: string | number;
|
|
17
|
+
readonly dropColumnId: string | number | null;
|
|
18
|
+
readonly beforeCardId: string | number | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type FluxKanbanInjection = {
|
|
22
|
+
readonly dragState: Ref<FluxKanbanDragState | null>;
|
|
23
|
+
|
|
24
|
+
registerCard(element: Element, cardId: string | number): void;
|
|
25
|
+
unregisterCard(element: Element): void;
|
|
26
|
+
getCardInfo(element: Element): { readonly cardId: string | number } | undefined;
|
|
27
|
+
startDrag(cardId: string | number, fromColumnId: string | number): void;
|
|
28
|
+
endDrag(): void;
|
|
29
|
+
updateDropTarget(columnId: string | number, beforeCardId: string | number | null): void;
|
|
30
|
+
clearDropTarget(): void;
|
|
31
|
+
commitDrop(): void;
|
|
32
|
+
};
|
|
33
|
+
|
|
13
34
|
export type FluxAdaptiveGroupChild = {
|
|
14
35
|
readonly priority: Ref<number>;
|
|
15
36
|
readonly desiredDefaultWidth: Ref<number>;
|