@heyramzi/tasks 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +285 -2
- package/package.json +98 -3
- package/src/ambient.d.ts +6 -0
- package/src/board-order.ts +137 -0
- package/src/dates.ts +31 -0
- package/src/define-statuses.ts +32 -0
- package/src/demo.svelte +394 -0
- package/src/icons/board-icon.svelte +11 -0
- package/src/icons/chevron-down.svg +1 -0
- package/src/icons/chevron-right.svg +1 -0
- package/src/icons/clock.svg +1 -0
- package/src/icons/close.svg +1 -0
- package/src/icons/gantt-icon.svelte +11 -0
- package/src/icons/list-icon.svelte +16 -0
- package/src/icons/milestone-flag.svg +1 -0
- package/src/icons/play.svg +1 -0
- package/src/icons/spinner.svg +1 -0
- package/src/inbox/icons/call.svg +1 -0
- package/src/inbox/icons/email.svg +1 -0
- package/src/inbox/icons/form.svg +1 -0
- package/src/inbox/icons/slack.svg +1 -0
- package/src/inbox/inbox-card.svelte +279 -0
- package/src/inbox/inbox-draft.svelte +84 -0
- package/src/inbox/inbox-logic.ts +56 -0
- package/src/inbox/inbox-statuses.ts +50 -0
- package/src/inbox/inbox-trace.svelte +44 -0
- package/src/inbox/inbox-types.ts +108 -0
- package/src/inbox/inbox.svelte +203 -0
- package/src/inbox/index.ts +23 -0
- package/src/index.ts +78 -0
- package/src/kanban-board.svelte +560 -0
- package/src/kanban-column.svelte +117 -0
- package/src/kanban-drag.ts +38 -0
- package/src/kanban-types.ts +65 -0
- package/src/lib/cn.ts +11 -0
- package/src/presets/sales.ts +16 -0
- package/src/reorder.ts +20 -0
- package/src/status-badge.svelte +57 -0
- package/src/status-dot.svelte +228 -0
- package/src/status-icon.svelte +30 -0
- package/src/status-types.ts +42 -0
- package/src/task-gantt.svelte +845 -0
- package/src/task-list.svelte +463 -0
- package/src/task-types.ts +43 -0
- package/src/task-view-switch.svelte +118 -0
- package/src/time/duration.ts +69 -0
- package/src/time/time-chip.svelte +62 -0
- package/src/time/time-dot.svelte +58 -0
- package/src/time/time-ring.svelte +74 -0
- package/src/time/time-tracker.svelte +567 -0
- package/src/time/time-types.ts +74 -0
- package/src/tone.ts +54 -0
- package/src/use-kanban-dnd.svelte.ts +173 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { boardOrderAt } from "./board-order.js";
|
|
2
|
+
import type { KanbanMove } from "./kanban-types.js";
|
|
3
|
+
|
|
4
|
+
export type KanbanDragOver = { columnId: string; overId: string | null };
|
|
5
|
+
|
|
6
|
+
export type KanbanDndConfig<T> = {
|
|
7
|
+
items: () => readonly T[];
|
|
8
|
+
getId: (item: T) => string;
|
|
9
|
+
getColumn: (item: T) => string;
|
|
10
|
+
getColumnItems: (columnId: string) => readonly T[];
|
|
11
|
+
/** The active sort. `"manual"` orders by `boardOrder`; anything else is a move only. */
|
|
12
|
+
getSortKey: () => string;
|
|
13
|
+
boardOrder?: (item: T) => string | null | undefined;
|
|
14
|
+
onMove: (id: string, move: KanbanMove) => void | Promise<void>;
|
|
15
|
+
/** A capped column hides its tail, so a drop on its empty space should land on top. */
|
|
16
|
+
dropAtTop?: (columnId: string) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Return false for display-only lanes (e.g. an Overdue lane). The board then
|
|
19
|
+
* never calls preventDefault for that column, so it is not a drop target.
|
|
20
|
+
*/
|
|
21
|
+
isDroppable?: (columnId: string) => boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The drag state and handlers for a board. Native HTML5 drag is deliberate:
|
|
26
|
+
* it reuses the browser's own drop targets, so a card can leave the board and
|
|
27
|
+
* land on any other element a caller wired up.
|
|
28
|
+
*/
|
|
29
|
+
export function createKanbanDnd<T>(config: KanbanDndConfig<T>) {
|
|
30
|
+
let draggedId = $state<string | null>(null);
|
|
31
|
+
let dragOver = $state<KanbanDragOver | null>(null);
|
|
32
|
+
|
|
33
|
+
function readDraggedId(e: DragEvent): string | null {
|
|
34
|
+
const raw = e.dataTransfer?.getData("text/plain");
|
|
35
|
+
if (!raw) return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(raw) as { id?: string };
|
|
38
|
+
return parsed.id ?? null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onDragStart(e: DragEvent, id: string): void {
|
|
45
|
+
if (e.dataTransfer) {
|
|
46
|
+
e.dataTransfer.effectAllowed = "move";
|
|
47
|
+
e.dataTransfer.setData("text/plain", JSON.stringify({ id }));
|
|
48
|
+
}
|
|
49
|
+
// Defer: marking the source during the dragstart event makes some browsers
|
|
50
|
+
// snapshot the faded state instead of the card.
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
draggedId = id;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onItemDragOver(
|
|
57
|
+
e: DragEvent,
|
|
58
|
+
columnId: string,
|
|
59
|
+
overId: string,
|
|
60
|
+
nextId: string | null,
|
|
61
|
+
): void {
|
|
62
|
+
if (config.isDroppable && !config.isDroppable(columnId)) return;
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
66
|
+
|
|
67
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
68
|
+
const insertAfter = e.clientY > rect.top + rect.height / 2;
|
|
69
|
+
const targetOverId = insertAfter ? nextId : overId;
|
|
70
|
+
|
|
71
|
+
if (targetOverId === draggedId) return;
|
|
72
|
+
if (dragOver?.columnId === columnId && dragOver.overId === targetOverId) return;
|
|
73
|
+
dragOver = { columnId, overId: targetOverId };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onColumnDragOver(e: DragEvent, columnId: string): void {
|
|
77
|
+
if (config.isDroppable && !config.isDroppable(columnId)) return;
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
80
|
+
if (dragOver?.columnId !== columnId || dragOver.overId !== null) {
|
|
81
|
+
dragOver = { columnId, overId: null };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function onDragLeave(e: DragEvent): void {
|
|
86
|
+
if (!(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) {
|
|
87
|
+
dragOver = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clearDragOver(): void {
|
|
92
|
+
dragOver = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resetDrag(): void {
|
|
96
|
+
draggedId = null;
|
|
97
|
+
dragOver = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Where a manual drop lands: on the hovered card, else the top or the end. */
|
|
101
|
+
function manualInsertIndex(
|
|
102
|
+
column: readonly T[],
|
|
103
|
+
overId: string | null,
|
|
104
|
+
columnId: string,
|
|
105
|
+
): number {
|
|
106
|
+
const overIndex = overId ? column.findIndex((item) => config.getId(item) === overId) : -1;
|
|
107
|
+
if (overIndex !== -1) return overIndex;
|
|
108
|
+
return config.dropAtTop?.(columnId) ? 0 : column.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onDrop(e: DragEvent, targetColumnId: string): void {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
if (config.isDroppable && !config.isDroppable(targetColumnId)) return;
|
|
114
|
+
const overId = dragOver?.overId ?? null;
|
|
115
|
+
dragOver = null;
|
|
116
|
+
draggedId = null;
|
|
117
|
+
|
|
118
|
+
const id = readDraggedId(e);
|
|
119
|
+
const item = id ? config.items().find((entry) => config.getId(entry) === id) : undefined;
|
|
120
|
+
if (!item) return;
|
|
121
|
+
|
|
122
|
+
const movedId = config.getId(item);
|
|
123
|
+
|
|
124
|
+
if (config.getSortKey() !== "manual") {
|
|
125
|
+
if (config.getColumn(item) !== targetColumnId) {
|
|
126
|
+
void config.onMove(movedId, { columnId: targetColumnId });
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const column = config
|
|
132
|
+
.getColumnItems(targetColumnId)
|
|
133
|
+
.filter((entry) => config.getId(entry) !== movedId);
|
|
134
|
+
const index = manualInsertIndex(column, overId, targetColumnId);
|
|
135
|
+
const before = column[index - 1];
|
|
136
|
+
const after = column[index];
|
|
137
|
+
const orderOf = config.boardOrder;
|
|
138
|
+
void config.onMove(movedId, {
|
|
139
|
+
columnId: targetColumnId,
|
|
140
|
+
...(orderOf ? { boardOrder: boardOrderAt(column, index, orderOf) } : {}),
|
|
141
|
+
beforeId: before === undefined ? null : config.getId(before),
|
|
142
|
+
afterId: after === undefined ? null : config.getId(after),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
get draggedId() {
|
|
148
|
+
return draggedId;
|
|
149
|
+
},
|
|
150
|
+
get dragOver() {
|
|
151
|
+
return dragOver;
|
|
152
|
+
},
|
|
153
|
+
set dragOver(value: KanbanDragOver | null) {
|
|
154
|
+
dragOver = value;
|
|
155
|
+
},
|
|
156
|
+
onDragStart,
|
|
157
|
+
onItemDragOver,
|
|
158
|
+
onColumnDragOver,
|
|
159
|
+
onDragLeave,
|
|
160
|
+
onDrop,
|
|
161
|
+
clearDragOver,
|
|
162
|
+
resetDrag,
|
|
163
|
+
isDragSource(id: string): boolean {
|
|
164
|
+
return draggedId === id;
|
|
165
|
+
},
|
|
166
|
+
showIndicatorBefore(columnId: string, itemId: string): boolean {
|
|
167
|
+
return dragOver?.columnId === columnId && dragOver.overId === itemId && draggedId !== null;
|
|
168
|
+
},
|
|
169
|
+
showIndicatorEnd(columnId: string): boolean {
|
|
170
|
+
return dragOver?.columnId === columnId && dragOver.overId === null && draggedId !== null;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|