@joewinke/jatui 0.1.28 → 0.1.30
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/dist/actions/collapse.js +15 -29
- package/dist/actions/listNav.js +108 -137
- package/dist/actions/railNav.js +72 -89
- package/dist/components/markup.js +59 -62
- package/dist/components/session-nav/transcriptModel.js +49 -66
- package/dist/data/emojis.js +4 -4
- package/dist/dnd/DropIndicator.svelte +119 -0
- package/dist/dnd/DropIndicator.svelte.d.ts +16 -0
- package/dist/dnd/announce.d.ts +1 -0
- package/dist/dnd/announce.js +1 -0
- package/dist/dnd/autoScroll.d.ts +7 -0
- package/dist/dnd/autoScroll.js +25 -0
- package/dist/dnd/createDndBoard.svelte.d.ts +43 -0
- package/dist/dnd/createDndBoard.svelte.js +183 -0
- package/dist/dnd/createDndBoard.test.d.ts +1 -0
- package/dist/dnd/createDndBoard.test.js +185 -0
- package/dist/dnd/draggable.d.ts +13 -0
- package/dist/dnd/draggable.js +42 -0
- package/dist/dnd/dropTarget.d.ts +14 -0
- package/dist/dnd/dropTarget.js +31 -0
- package/dist/dnd/index.d.ts +13 -0
- package/dist/dnd/index.js +9 -0
- package/dist/dnd/types.d.ts +41 -0
- package/dist/dnd/types.js +14 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/stores/confirmDialog.svelte.js +5 -5
- package/dist/stores/inputDialog.svelte.js +5 -5
- package/dist/types/booking.js +4 -4
- package/dist/types/comments.js +2 -2
- package/dist/types/messaging.js +3 -3
- package/dist/types/pipeline.js +2 -2
- package/dist/utils/currency.js +2 -4
- package/dist/utils/daisyuiColors.js +25 -27
- package/dist/utils/dateFormatters.js +44 -44
- package/dist/utils/googleMapsLoader.js +46 -100
- package/dist/utils/mentionParser.js +30 -30
- package/dist/utils/phone.js +21 -27
- package/dist/utils/phoneFormat.js +9 -9
- package/dist/utils/taskUtils.js +1 -1
- package/package.json +25 -3
|
@@ -22,17 +22,6 @@
|
|
|
22
22
|
* transcript shows just the headline by default and the body is
|
|
23
23
|
* a drill-down. This is what keeps the left column readable.
|
|
24
24
|
*/
|
|
25
|
-
var __assign = (this && this.__assign) || function () {
|
|
26
|
-
__assign = Object.assign || function(t) {
|
|
27
|
-
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
28
|
-
s = arguments[i];
|
|
29
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
30
|
-
t[p] = s[p];
|
|
31
|
-
}
|
|
32
|
-
return t;
|
|
33
|
-
};
|
|
34
|
-
return __assign.apply(this, arguments);
|
|
35
|
-
};
|
|
36
25
|
export function rawTypeToState(raw) {
|
|
37
26
|
switch (raw) {
|
|
38
27
|
case 'needs_input':
|
|
@@ -48,28 +37,27 @@ export function rawTypeToState(raw) {
|
|
|
48
37
|
}
|
|
49
38
|
/** Detect a `jat-signal <type> '<json>'` invocation in a tool-call body. */
|
|
50
39
|
function parseSignalBody(body) {
|
|
51
|
-
|
|
40
|
+
const m = body.match(/jat-signal\s+(starting|working|needs_input|review|completing|complete|completed|paused|idle)\b/);
|
|
52
41
|
if (!m)
|
|
53
42
|
return null;
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
let label = '';
|
|
44
|
+
const pick = body.match(/"(?:approach|question|reviewFocus|currentStep|summary|reason|taskTitle)"\s*:\s*"([^"]{3,90})/);
|
|
56
45
|
if (pick)
|
|
57
46
|
label = pick[1];
|
|
58
|
-
return { rawType: m[1], label
|
|
47
|
+
return { rawType: m[1], label };
|
|
59
48
|
}
|
|
60
49
|
/** Build the linked model from rendered transcript lines. */
|
|
61
50
|
export function buildLinkedSession(lines) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
const items = [];
|
|
52
|
+
const emits = [];
|
|
53
|
+
let cur = null;
|
|
54
|
+
const flush = () => {
|
|
66
55
|
if (cur) {
|
|
67
|
-
items.push(
|
|
56
|
+
items.push({ id: items.length, ...cur, signalId: null });
|
|
68
57
|
cur = null;
|
|
69
58
|
}
|
|
70
59
|
};
|
|
71
|
-
for (
|
|
72
|
-
var line = lines_1[_i];
|
|
60
|
+
for (const line of lines) {
|
|
73
61
|
if (!line.trim()) {
|
|
74
62
|
flush();
|
|
75
63
|
continue;
|
|
@@ -80,8 +68,8 @@ export function buildLinkedSession(lines) {
|
|
|
80
68
|
continue;
|
|
81
69
|
}
|
|
82
70
|
if (line.startsWith('⏺ ')) {
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
const body = line.slice(2);
|
|
72
|
+
const sig = parseSignalBody(body);
|
|
85
73
|
if (sig) {
|
|
86
74
|
// signal emission = boundary; consume it, don't render as an item
|
|
87
75
|
flush();
|
|
@@ -93,7 +81,7 @@ export function buildLinkedSession(lines) {
|
|
|
93
81
|
continue;
|
|
94
82
|
}
|
|
95
83
|
if (line.startsWith(' ⎿ ')) {
|
|
96
|
-
|
|
84
|
+
const body = line.slice(4);
|
|
97
85
|
if (cur) {
|
|
98
86
|
cur.detail.push(body);
|
|
99
87
|
cur.srcLines++;
|
|
@@ -102,7 +90,7 @@ export function buildLinkedSession(lines) {
|
|
|
102
90
|
}
|
|
103
91
|
if (line.startsWith(' ')) {
|
|
104
92
|
// continuation: tool detail for actions, prose for prompt/assistant
|
|
105
|
-
|
|
93
|
+
const body = line.trimStart();
|
|
106
94
|
if (cur && cur.kind === 'action') {
|
|
107
95
|
cur.detail.push(body);
|
|
108
96
|
cur.srcLines++;
|
|
@@ -125,21 +113,21 @@ export function buildLinkedSession(lines) {
|
|
|
125
113
|
}
|
|
126
114
|
flush();
|
|
127
115
|
// Build signal nodes from emission boundaries.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
emits.forEach(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for (
|
|
116
|
+
const signals = [];
|
|
117
|
+
let prevBoundary = -1;
|
|
118
|
+
emits.forEach((e, i) => {
|
|
119
|
+
const rowStart = Math.max(0, prevBoundary + 1);
|
|
120
|
+
const rowEnd = Math.max(rowStart, e.afterIdx);
|
|
121
|
+
let lineCount = 0;
|
|
122
|
+
for (let k = rowStart; k <= rowEnd && k < items.length; k++)
|
|
135
123
|
lineCount += items[k].srcLines;
|
|
136
124
|
signals.push({
|
|
137
|
-
id:
|
|
125
|
+
id: `sig-${i}`,
|
|
138
126
|
state: rawTypeToState(e.rawType),
|
|
139
127
|
rawType: e.rawType,
|
|
140
128
|
label: e.label,
|
|
141
|
-
rowStart
|
|
142
|
-
rowEnd
|
|
129
|
+
rowStart,
|
|
130
|
+
rowEnd,
|
|
143
131
|
lineCount: Math.max(1, lineCount),
|
|
144
132
|
// No WorkBlock substrate data in the raw-lines path
|
|
145
133
|
blockId: null,
|
|
@@ -151,13 +139,12 @@ export function buildLinkedSession(lines) {
|
|
|
151
139
|
});
|
|
152
140
|
prevBoundary = e.afterIdx;
|
|
153
141
|
});
|
|
154
|
-
for (
|
|
155
|
-
|
|
156
|
-
for (var k = sig.rowStart; k <= sig.rowEnd && k < items.length; k++) {
|
|
142
|
+
for (const sig of signals) {
|
|
143
|
+
for (let k = sig.rowStart; k <= sig.rowEnd && k < items.length; k++) {
|
|
157
144
|
items[k].signalId = sig.id;
|
|
158
145
|
}
|
|
159
146
|
}
|
|
160
|
-
return { items
|
|
147
|
+
return { items, signals };
|
|
161
148
|
}
|
|
162
149
|
/**
|
|
163
150
|
* Build a LinkedSession from the WorkBlock substrate (B1).
|
|
@@ -175,49 +162,45 @@ export function buildLinkedSession(lines) {
|
|
|
175
162
|
*/
|
|
176
163
|
export function buildLinkedSessionFromWork(blocks, items) {
|
|
177
164
|
// Map WorkTranscriptItem → TranscriptItem (blockId → signalId mapping happens after)
|
|
178
|
-
|
|
165
|
+
const transcriptItems = items.map((it) => ({
|
|
179
166
|
id: it.id,
|
|
180
167
|
kind: it.kind,
|
|
181
168
|
headline: it.headline,
|
|
182
169
|
detail: it.detail,
|
|
183
170
|
srcLines: it.srcLines,
|
|
184
171
|
signalId: null // filled in below
|
|
185
|
-
})
|
|
172
|
+
}));
|
|
186
173
|
// Map WorkBlock → SignalNode
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
linesRemoved: (_c = blk.linesRemoved) !== null && _c !== void 0 ? _c : 0
|
|
204
|
-
});
|
|
205
|
-
});
|
|
174
|
+
const signals = blocks.map((blk) => ({
|
|
175
|
+
id: blk.id,
|
|
176
|
+
state: rawTypeToState(blk.signal.type),
|
|
177
|
+
rawType: blk.signal.type,
|
|
178
|
+
label: blk.signal.label,
|
|
179
|
+
rowStart: blk.itemRange.start,
|
|
180
|
+
rowEnd: blk.itemRange.end,
|
|
181
|
+
lineCount: blk.lineCount,
|
|
182
|
+
// WorkBlock substrate fields
|
|
183
|
+
blockId: blk.id,
|
|
184
|
+
summary: blk.summary,
|
|
185
|
+
filesTouched: blk.filesTouched,
|
|
186
|
+
commits: blk.commits ?? [],
|
|
187
|
+
linesAdded: blk.linesAdded ?? 0,
|
|
188
|
+
linesRemoved: blk.linesRemoved ?? 0
|
|
189
|
+
}));
|
|
206
190
|
// Assign signalId to transcript items via block ownership
|
|
207
|
-
for (
|
|
208
|
-
|
|
209
|
-
for (var k = sig.rowStart; k <= sig.rowEnd && k < transcriptItems.length; k++) {
|
|
191
|
+
for (const sig of signals) {
|
|
192
|
+
for (let k = sig.rowStart; k <= sig.rowEnd && k < transcriptItems.length; k++) {
|
|
210
193
|
transcriptItems[k].signalId = sig.id;
|
|
211
194
|
}
|
|
212
195
|
}
|
|
213
|
-
return { items: transcriptItems, signals
|
|
196
|
+
return { items: transcriptItems, signals };
|
|
214
197
|
}
|
|
215
198
|
/**
|
|
216
199
|
* A baked, realistic sample so /sankey always demos beautifully even with no live
|
|
217
200
|
* session. Authored as raw transcript lines with real `jat-signal` calls, so it
|
|
218
201
|
* flows through the EXACT same parser as live data.
|
|
219
202
|
*/
|
|
220
|
-
export
|
|
203
|
+
export const SAMPLE_TRANSCRIPT_LINES = [
|
|
221
204
|
'',
|
|
222
205
|
'› /jat:start GentleCoast jat-7h2k',
|
|
223
206
|
'⏺ Bash(get-current-session-id)',
|
package/dist/data/emojis.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Each emoji has a character and searchable name.
|
|
4
4
|
* Based on Unicode CLDR emoji ordering, minus most flags.
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
6
|
+
export const EMOJI_DATA = [
|
|
7
7
|
{
|
|
8
8
|
label: 'Smileys & Emotion',
|
|
9
9
|
emojis: [
|
|
@@ -1186,11 +1186,11 @@ export var EMOJI_DATA = [
|
|
|
1186
1186
|
},
|
|
1187
1187
|
];
|
|
1188
1188
|
/** Flat array of all emojis for search */
|
|
1189
|
-
export
|
|
1189
|
+
export const ALL_EMOJIS = EMOJI_DATA.flatMap((g) => g.emojis);
|
|
1190
1190
|
/** Search emojis by name, returns matching entries */
|
|
1191
1191
|
export function searchEmojis(query) {
|
|
1192
1192
|
if (!query.trim())
|
|
1193
1193
|
return [];
|
|
1194
|
-
|
|
1195
|
-
return ALL_EMOJIS.filter(
|
|
1194
|
+
const terms = query.toLowerCase().split(/\s+/);
|
|
1195
|
+
return ALL_EMOJIS.filter((e) => terms.every((t) => e.name.includes(t)));
|
|
1196
1196
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Drop-edge indicator — Svelte port of pdnd's react-drop-indicator Line.
|
|
4
|
+
* Render conditionally: {#if closestEdge}<DropIndicator edge={closestEdge} gap="0.5rem" />{/if}
|
|
5
|
+
* Host item wrapper MUST be `position: relative`.
|
|
6
|
+
*/
|
|
7
|
+
import type { Edge } from './types';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
edge,
|
|
11
|
+
gap = '0px',
|
|
12
|
+
indent = '0px',
|
|
13
|
+
type = 'terminal',
|
|
14
|
+
appearance = 'default',
|
|
15
|
+
}: {
|
|
16
|
+
edge: Edge;
|
|
17
|
+
gap?: string;
|
|
18
|
+
indent?: string;
|
|
19
|
+
type?: 'terminal' | 'no-terminal' | 'terminal-no-bleed';
|
|
20
|
+
appearance?: 'default' | 'warning';
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
const orientation = $derived(edge === 'top' || edge === 'bottom' ? 'horizontal' : 'vertical');
|
|
24
|
+
|
|
25
|
+
const lineMainAxisStart = $derived(
|
|
26
|
+
type === 'terminal'
|
|
27
|
+
? `calc(var(--terminal-radius) + ${indent})`
|
|
28
|
+
: type === 'terminal-no-bleed'
|
|
29
|
+
? `calc(var(--terminal-diameter) + ${indent})`
|
|
30
|
+
: indent,
|
|
31
|
+
);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
class="drop-indicator {orientation} edge-{edge}"
|
|
36
|
+
style:--stroke-color={appearance === 'warning' ? 'var(--color-warning)' : 'var(--color-primary)'}
|
|
37
|
+
style:--main-axis-offset="calc(-0.5 * ({gap} + var(--stroke-width)))"
|
|
38
|
+
style:--line-main-axis-start={lineMainAxisStart}
|
|
39
|
+
style:--terminal-display={type === 'no-terminal' ? 'none' : 'block'}
|
|
40
|
+
></div>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.drop-indicator {
|
|
44
|
+
--stroke-width: 2px;
|
|
45
|
+
--terminal-diameter: calc(var(--stroke-width) * 4);
|
|
46
|
+
--terminal-radius: calc(var(--terminal-diameter) / 2);
|
|
47
|
+
--terminal-main-axis-start: calc(-1 * var(--terminal-diameter));
|
|
48
|
+
--terminal-cross-axis-offset: calc((var(--stroke-width) - var(--terminal-diameter)) / 2);
|
|
49
|
+
|
|
50
|
+
display: block;
|
|
51
|
+
position: absolute;
|
|
52
|
+
z-index: 1;
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
background: var(--stroke-color);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.drop-indicator::before {
|
|
58
|
+
display: var(--terminal-display);
|
|
59
|
+
content: '';
|
|
60
|
+
position: absolute;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
width: var(--terminal-diameter);
|
|
63
|
+
height: var(--terminal-diameter);
|
|
64
|
+
border: var(--stroke-width) solid var(--stroke-color);
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.horizontal {
|
|
69
|
+
height: var(--stroke-width);
|
|
70
|
+
inset-inline-start: var(--line-main-axis-start);
|
|
71
|
+
inset-inline-end: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.horizontal::before {
|
|
75
|
+
inset-inline-start: var(--terminal-main-axis-start);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.vertical {
|
|
79
|
+
width: var(--stroke-width);
|
|
80
|
+
top: var(--line-main-axis-start);
|
|
81
|
+
bottom: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.vertical::before {
|
|
85
|
+
top: var(--terminal-main-axis-start);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.edge-top {
|
|
89
|
+
top: var(--main-axis-offset);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.edge-top::before {
|
|
93
|
+
top: var(--terminal-cross-axis-offset);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.edge-bottom {
|
|
97
|
+
bottom: var(--main-axis-offset);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.edge-bottom::before {
|
|
101
|
+
bottom: var(--terminal-cross-axis-offset);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.edge-left {
|
|
105
|
+
left: var(--main-axis-offset);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.edge-left::before {
|
|
109
|
+
left: var(--terminal-cross-axis-offset);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.edge-right {
|
|
113
|
+
right: var(--main-axis-offset);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.edge-right::before {
|
|
117
|
+
right: var(--terminal-cross-axis-offset);
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-edge indicator — Svelte port of pdnd's react-drop-indicator Line.
|
|
3
|
+
* Render conditionally: {#if closestEdge}<DropIndicator edge={closestEdge} gap="0.5rem" />{/if}
|
|
4
|
+
* Host item wrapper MUST be `position: relative`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Edge } from './types';
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
edge: Edge;
|
|
9
|
+
gap?: string;
|
|
10
|
+
indent?: string;
|
|
11
|
+
type?: 'terminal' | 'no-terminal' | 'terminal-no-bleed';
|
|
12
|
+
appearance?: 'default' | 'warning';
|
|
13
|
+
};
|
|
14
|
+
declare const DropIndicator: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type DropIndicator = ReturnType<typeof DropIndicator>;
|
|
16
|
+
export default DropIndicator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { announce, cleanup as cleanupAnnouncements } from '@atlaskit/pragmatic-drag-and-drop-live-region';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { announce, cleanup as cleanupAnnouncements } from '@atlaskit/pragmatic-drag-and-drop-live-region';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
|
2
|
+
export const autoScroll = (node, params) => {
|
|
3
|
+
let current = params ?? {};
|
|
4
|
+
function register() {
|
|
5
|
+
return autoScrollForElements({
|
|
6
|
+
element: node,
|
|
7
|
+
canScroll: ({ source }) => {
|
|
8
|
+
if (!current.types?.length)
|
|
9
|
+
return true;
|
|
10
|
+
return current.types.includes(source.data.type);
|
|
11
|
+
},
|
|
12
|
+
getAllowedAxis: () => current.axis ?? 'all',
|
|
13
|
+
getConfiguration: () => ({ maxScrollSpeed: current.speed ?? 'standard' }),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
let cleanup = register();
|
|
17
|
+
return {
|
|
18
|
+
update(next) {
|
|
19
|
+
current = next ?? {};
|
|
20
|
+
cleanup();
|
|
21
|
+
cleanup = register();
|
|
22
|
+
},
|
|
23
|
+
destroy: () => cleanup(),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
2
|
+
import { type ItemDragData, type ContainerDropData } from './types';
|
|
3
|
+
import type { DraggableParams } from './draggable';
|
|
4
|
+
import type { DropTargetParams } from './dropTarget';
|
|
5
|
+
export interface BoardMove<TItem> {
|
|
6
|
+
item: TItem;
|
|
7
|
+
from: {
|
|
8
|
+
container: string;
|
|
9
|
+
index: number;
|
|
10
|
+
};
|
|
11
|
+
to: {
|
|
12
|
+
container: string;
|
|
13
|
+
index: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface DndBoardOptions<TItem> {
|
|
17
|
+
type: string;
|
|
18
|
+
items: () => ReadonlyMap<string, readonly TItem[]> | Record<string, readonly TItem[]>;
|
|
19
|
+
key: (item: TItem) => string;
|
|
20
|
+
commit: (move: BoardMove<TItem>) => Promise<void>;
|
|
21
|
+
onError?: (error: unknown, move: BoardMove<TItem>) => void;
|
|
22
|
+
announce?: (move: BoardMove<TItem>) => string;
|
|
23
|
+
hover?: 'indicator' | 'shadow';
|
|
24
|
+
edges?: Edge[];
|
|
25
|
+
}
|
|
26
|
+
export declare function createDndBoard<TItem>(opts: DndBoardOptions<TItem>): {
|
|
27
|
+
get: (container: string) => readonly TItem[];
|
|
28
|
+
containers: () => string[];
|
|
29
|
+
readonly dragging: {
|
|
30
|
+
key: string;
|
|
31
|
+
item: TItem;
|
|
32
|
+
from: BoardMove<TItem>["from"];
|
|
33
|
+
} | null;
|
|
34
|
+
readonly proposal: {
|
|
35
|
+
container: string;
|
|
36
|
+
index: number;
|
|
37
|
+
} | null;
|
|
38
|
+
isSource: (item: TItem) => boolean;
|
|
39
|
+
isShadow: (item: TItem) => boolean;
|
|
40
|
+
item: (item: TItem, container: string, index: number) => DraggableParams<ItemDragData<TItem>>;
|
|
41
|
+
itemTarget: (container: string, index: number) => DropTargetParams<ContainerDropData>;
|
|
42
|
+
containerTarget: (container: string) => DropTargetParams<ContainerDropData>;
|
|
43
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
2
|
+
import { extractClosestEdge, attachClosestEdge, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
|
3
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
4
|
+
import { announce } from './announce';
|
|
5
|
+
import { makeItemData, isItemData, } from './types';
|
|
6
|
+
const SHADOW_KEY = '__dnd-shadow__';
|
|
7
|
+
export function createDndBoard(opts) {
|
|
8
|
+
const edges = opts.edges ?? ['top', 'bottom'];
|
|
9
|
+
const hover = opts.hover ?? 'indicator';
|
|
10
|
+
const mirror = new SvelteMap();
|
|
11
|
+
let dragging = $state(null);
|
|
12
|
+
let proposal = $state(null);
|
|
13
|
+
const seq = new Map();
|
|
14
|
+
let inFlight = 0;
|
|
15
|
+
let seedQueued = false;
|
|
16
|
+
function seed() {
|
|
17
|
+
mirror.clear();
|
|
18
|
+
const src = opts.items();
|
|
19
|
+
const entries = src instanceof Map
|
|
20
|
+
? src
|
|
21
|
+
: new Map(Object.entries(src));
|
|
22
|
+
for (const [id, list] of entries)
|
|
23
|
+
mirror.set(id, [...list]);
|
|
24
|
+
}
|
|
25
|
+
$effect(() => {
|
|
26
|
+
opts.items();
|
|
27
|
+
if (dragging || inFlight > 0) {
|
|
28
|
+
seedQueued = true;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
seed();
|
|
32
|
+
});
|
|
33
|
+
function placementFrom(dropTargets) {
|
|
34
|
+
const inner = dropTargets[0];
|
|
35
|
+
if (!inner)
|
|
36
|
+
return null;
|
|
37
|
+
const d = inner.data;
|
|
38
|
+
if (typeof d.container !== 'string')
|
|
39
|
+
return null;
|
|
40
|
+
const list = mirror.get(d.container) ?? [];
|
|
41
|
+
if (typeof d.index !== 'number')
|
|
42
|
+
return { container: d.container, index: list.length };
|
|
43
|
+
const edge = extractClosestEdge(d);
|
|
44
|
+
const after = edge === 'bottom' || edge === 'right';
|
|
45
|
+
return { container: d.container, index: d.index + (after ? 1 : 0) };
|
|
46
|
+
}
|
|
47
|
+
function syncShadow(next) {
|
|
48
|
+
for (const [cid, list] of mirror) {
|
|
49
|
+
const idx = list.findIndex((item) => item[SHADOW_KEY]);
|
|
50
|
+
if (idx !== -1) {
|
|
51
|
+
list.splice(idx, 1);
|
|
52
|
+
mirror.set(cid, list);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!next || !dragging)
|
|
57
|
+
return;
|
|
58
|
+
const list = [...(mirror.get(next.container) ?? [])];
|
|
59
|
+
const shadowItem = { [SHADOW_KEY]: true, key: `${SHADOW_KEY}-${dragging.key}` };
|
|
60
|
+
list.splice(Math.min(next.index, list.length), 0, shadowItem);
|
|
61
|
+
mirror.set(next.container, list);
|
|
62
|
+
}
|
|
63
|
+
function consider(next) {
|
|
64
|
+
if (next?.container === proposal?.container && next?.index === proposal?.index)
|
|
65
|
+
return;
|
|
66
|
+
proposal = next;
|
|
67
|
+
if (hover === 'shadow')
|
|
68
|
+
syncShadow(next);
|
|
69
|
+
}
|
|
70
|
+
function applyToMirror(move) {
|
|
71
|
+
const fromList = [...(mirror.get(move.from.container) ?? [])];
|
|
72
|
+
const toList = move.from.container === move.to.container
|
|
73
|
+
? fromList
|
|
74
|
+
: [...(mirror.get(move.to.container) ?? [])];
|
|
75
|
+
const srcIdx = fromList.findIndex((it) => opts.key(it) === opts.key(move.item));
|
|
76
|
+
if (srcIdx === -1)
|
|
77
|
+
return;
|
|
78
|
+
fromList.splice(srcIdx, 1);
|
|
79
|
+
let destIdx = move.to.index;
|
|
80
|
+
if (move.from.container === move.to.container && move.to.index > move.from.index) {
|
|
81
|
+
destIdx = Math.max(0, destIdx - 1);
|
|
82
|
+
}
|
|
83
|
+
toList.splice(Math.min(destIdx, toList.length), 0, move.item);
|
|
84
|
+
mirror.set(move.from.container, fromList);
|
|
85
|
+
if (move.from.container !== move.to.container) {
|
|
86
|
+
mirror.set(move.to.container, toList);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function rollback(move) {
|
|
90
|
+
for (const [cid, list] of mirror) {
|
|
91
|
+
const idx = list.findIndex((it) => opts.key(it) === opts.key(move.item));
|
|
92
|
+
if (idx !== -1) {
|
|
93
|
+
list.splice(idx, 1);
|
|
94
|
+
mirror.set(cid, list);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const fromList = [...(mirror.get(move.from.container) ?? [])];
|
|
99
|
+
fromList.splice(Math.min(move.from.index, fromList.length), 0, move.item);
|
|
100
|
+
mirror.set(move.from.container, fromList);
|
|
101
|
+
}
|
|
102
|
+
function finalize(dropped) {
|
|
103
|
+
const drag = dragging;
|
|
104
|
+
const to = dropped ? proposal : null;
|
|
105
|
+
dragging = null;
|
|
106
|
+
consider(null);
|
|
107
|
+
if (!drag)
|
|
108
|
+
return;
|
|
109
|
+
if (!to ||
|
|
110
|
+
(to.container === drag.from.container && to.index === drag.from.index)) {
|
|
111
|
+
if (seedQueued && inFlight === 0) {
|
|
112
|
+
seedQueued = false;
|
|
113
|
+
seed();
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const move = { item: drag.item, from: drag.from, to };
|
|
118
|
+
applyToMirror(move);
|
|
119
|
+
if (opts.announce)
|
|
120
|
+
announce(opts.announce(move));
|
|
121
|
+
const token = (seq.get(drag.key) ?? 0) + 1;
|
|
122
|
+
seq.set(drag.key, token);
|
|
123
|
+
inFlight++;
|
|
124
|
+
opts
|
|
125
|
+
.commit(move)
|
|
126
|
+
.catch((error) => {
|
|
127
|
+
if (seq.get(drag.key) === token)
|
|
128
|
+
rollback(move);
|
|
129
|
+
opts.onError?.(error, move);
|
|
130
|
+
})
|
|
131
|
+
.finally(() => {
|
|
132
|
+
inFlight--;
|
|
133
|
+
if (inFlight === 0 && seedQueued && !dragging) {
|
|
134
|
+
seedQueued = false;
|
|
135
|
+
seed();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
$effect(() => monitorForElements({
|
|
140
|
+
canMonitor: ({ source }) => isItemData(source.data, opts.type),
|
|
141
|
+
onDragStart({ source }) {
|
|
142
|
+
const d = source.data;
|
|
143
|
+
dragging = {
|
|
144
|
+
key: d.key,
|
|
145
|
+
item: d.item,
|
|
146
|
+
from: { container: d.container, index: d.index },
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
onDropTargetChange({ location }) {
|
|
150
|
+
consider(placementFrom(location.current.dropTargets));
|
|
151
|
+
},
|
|
152
|
+
onDrag({ location }) {
|
|
153
|
+
consider(placementFrom(location.current.dropTargets));
|
|
154
|
+
},
|
|
155
|
+
onDrop({ location }) {
|
|
156
|
+
finalize(location.current.dropTargets.length > 0);
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
return {
|
|
160
|
+
get: (container) => mirror.get(container) ?? [],
|
|
161
|
+
containers: () => [...mirror.keys()],
|
|
162
|
+
get dragging() {
|
|
163
|
+
return dragging;
|
|
164
|
+
},
|
|
165
|
+
get proposal() {
|
|
166
|
+
return proposal;
|
|
167
|
+
},
|
|
168
|
+
isSource: (item) => dragging?.key === opts.key(item),
|
|
169
|
+
isShadow: (item) => !!item[SHADOW_KEY],
|
|
170
|
+
item: (item, container, index) => ({
|
|
171
|
+
data: () => makeItemData({ type: opts.type, key: opts.key(item), item, container, index }),
|
|
172
|
+
}),
|
|
173
|
+
itemTarget: (container, index) => ({
|
|
174
|
+
data: (feedback) => attachClosestEdge({ type: `${opts.type}-target`, container, index }, { input: feedback.input, element: feedback.element, allowedEdges: edges }),
|
|
175
|
+
accepts: (d) => isItemData(d, opts.type),
|
|
176
|
+
sticky: true,
|
|
177
|
+
}),
|
|
178
|
+
containerTarget: (container) => ({
|
|
179
|
+
data: () => ({ type: `${opts.type}-target`, container }),
|
|
180
|
+
accepts: (d) => isItemData(d, opts.type),
|
|
181
|
+
}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|