@happyvertical/smrt-svelte 0.55.0 → 0.55.2
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/README.md +38 -6
- package/agents/components.md +44 -1
- package/agents/workspace.md +77 -2
- package/dist/components/app/AppShell.svelte +122 -24
- package/dist/components/app/AppShell.svelte.d.ts +25 -2
- package/dist/components/app/AppShell.svelte.d.ts.map +1 -1
- package/dist/components/app/DockToggles.svelte +103 -0
- package/dist/components/app/DockToggles.svelte.d.ts +8 -0
- package/dist/components/app/DockToggles.svelte.d.ts.map +1 -0
- package/dist/components/app/dock-toggle.d.ts +14 -0
- package/dist/components/app/dock-toggle.d.ts.map +1 -0
- package/dist/components/app/dock-toggle.js +1 -0
- package/dist/components/app/index.d.ts +2 -0
- package/dist/components/app/index.d.ts.map +1 -1
- package/dist/components/app/index.js +1 -0
- package/dist/components/board/Board.svelte +92 -394
- package/dist/components/board/Board.svelte.d.ts.map +1 -1
- package/dist/components/forms/RelationInput.svelte +379 -0
- package/dist/components/forms/RelationInput.svelte.d.ts +75 -0
- package/dist/components/forms/RelationInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +2 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +1 -0
- package/dist/components/forms/types.d.ts +9 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/sortable/Sortable.svelte +359 -0
- package/dist/components/sortable/Sortable.svelte.d.ts +26 -0
- package/dist/components/sortable/Sortable.svelte.d.ts.map +1 -0
- package/dist/components/sortable/controller.svelte.d.ts +152 -0
- package/dist/components/sortable/controller.svelte.d.ts.map +1 -0
- package/dist/components/sortable/controller.svelte.js +453 -0
- package/dist/components/sortable/index.d.ts +5 -0
- package/dist/components/sortable/index.d.ts.map +1 -0
- package/dist/components/sortable/index.js +2 -0
- package/dist/components/sortable/types.d.ts +80 -0
- package/dist/components/sortable/types.d.ts.map +1 -0
- package/dist/components/sortable/types.js +1 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte +62 -3
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +5 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +2 -1
- package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte +196 -0
- package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts +17 -0
- package/dist/components/workspace/admin-shell/ShellLayoutEditor.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/TenantNav.svelte +69 -3
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/dock.d.ts +37 -0
- package/dist/components/workspace/admin-shell/dock.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/dock.js +25 -0
- package/dist/components/workspace/admin-shell/layout-context.d.ts +7 -0
- package/dist/components/workspace/admin-shell/layout-context.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/layout-context.js +17 -0
- package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts +69 -0
- package/dist/components/workspace/admin-shell/layout-controller.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/layout-controller.svelte.js +91 -0
- package/dist/components/workspace/admin-shell/layout.d.ts +120 -0
- package/dist/components/workspace/admin-shell/layout.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/layout.js +402 -0
- package/dist/components/workspace/admin-shell/settings.d.ts +6 -4
- package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/settings.js +14 -4
- package/dist/components/workspace/admin-shell/state.svelte.d.ts +59 -1
- package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/state.svelte.js +146 -2
- package/dist/components/workspace/admin-shell/types.d.ts +52 -0
- package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
- package/dist/components/workspace/index.d.ts +6 -1
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +5 -0
- package/dist/i18n/server.d.ts +1 -0
- package/dist/i18n/server.d.ts.map +1 -1
- package/dist/i18n/server.js +1 -0
- package/dist/i18n/strings.sortable.d.ts +13 -0
- package/dist/i18n/strings.sortable.d.ts.map +1 -0
- package/dist/i18n/strings.sortable.js +13 -0
- package/dist/i18n/strings.workspace.d.ts +13 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +13 -0
- package/package.json +26 -15
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless drag-and-drop engine shared by `Board` and `Sortable`: keyboard
|
|
3
|
+
* pick-up and move, pointer (mouse, touch, pen) and native HTML drag, a
|
|
4
|
+
* single in-flight move at a time, focus return, and screen-reader
|
|
5
|
+
* announcements. It knows nothing about cards, columns, or navigation: a host
|
|
6
|
+
* describes ordered containers of item ids and receives one typed move.
|
|
7
|
+
*
|
|
8
|
+
* Create it during component initialization (it registers an effect that
|
|
9
|
+
* returns focus to the moved item).
|
|
10
|
+
*/
|
|
11
|
+
import { BROWSER } from 'esm-env';
|
|
12
|
+
import { tick } from 'svelte';
|
|
13
|
+
function clamp(index, max) {
|
|
14
|
+
return Math.max(0, Math.min(index, max));
|
|
15
|
+
}
|
|
16
|
+
export class SortableController {
|
|
17
|
+
options;
|
|
18
|
+
drag = $state();
|
|
19
|
+
announcement = $state('');
|
|
20
|
+
focusItemId = $state();
|
|
21
|
+
suppressedSelectionId = $state();
|
|
22
|
+
pending = $state(false);
|
|
23
|
+
pointerSession = $state();
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (!BROWSER)
|
|
28
|
+
return;
|
|
29
|
+
if (!this.focusItemId)
|
|
30
|
+
return;
|
|
31
|
+
const itemId = this.focusItemId;
|
|
32
|
+
this.focusItemId = undefined;
|
|
33
|
+
tick().then(() => {
|
|
34
|
+
const root = this.options.root();
|
|
35
|
+
const element = root
|
|
36
|
+
? Array.from(root.querySelectorAll(this.options.selectors.item)).find((candidate) => candidate.dataset[this.options.selectors.itemKey] === itemId)
|
|
37
|
+
: undefined;
|
|
38
|
+
const target = element
|
|
39
|
+
? this.options.focusTarget
|
|
40
|
+
? this.options.focusTarget(element)
|
|
41
|
+
: element
|
|
42
|
+
: undefined;
|
|
43
|
+
target?.focus();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Whether a new move may start now. */
|
|
48
|
+
get movable() {
|
|
49
|
+
return this.options.enabled() && !this.pending;
|
|
50
|
+
}
|
|
51
|
+
/** Whether native HTML drag should be offered (not while a pointer session runs). */
|
|
52
|
+
get nativeDraggable() {
|
|
53
|
+
return this.movable && this.pointerSession === undefined;
|
|
54
|
+
}
|
|
55
|
+
get containers() {
|
|
56
|
+
return this.options.containers();
|
|
57
|
+
}
|
|
58
|
+
itemsIn(containerId, withoutId) {
|
|
59
|
+
const ids = this.options.itemIds(containerId);
|
|
60
|
+
return withoutId ? ids.filter((id) => id !== withoutId) : ids;
|
|
61
|
+
}
|
|
62
|
+
findContainer(id) {
|
|
63
|
+
return this.containers.find((container) => container.id === id);
|
|
64
|
+
}
|
|
65
|
+
positionFor(itemId) {
|
|
66
|
+
if (this.options.itemLabel(itemId) === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
for (const container of this.containers) {
|
|
69
|
+
const index = this.itemsIn(container.id).indexOf(itemId);
|
|
70
|
+
if (index >= 0)
|
|
71
|
+
return { containerId: container.id, index };
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
label(itemId) {
|
|
76
|
+
return this.options.itemLabel(itemId) ?? '';
|
|
77
|
+
}
|
|
78
|
+
say(announcement) {
|
|
79
|
+
this.announcement = this.options.announce(announcement);
|
|
80
|
+
}
|
|
81
|
+
pickup(itemId, mode) {
|
|
82
|
+
if (!this.movable)
|
|
83
|
+
return;
|
|
84
|
+
const source = this.positionFor(itemId);
|
|
85
|
+
if (!source)
|
|
86
|
+
return;
|
|
87
|
+
this.drag = { itemId, source, target: { ...source }, mode };
|
|
88
|
+
this.say({
|
|
89
|
+
type: 'pickup',
|
|
90
|
+
item: this.label(itemId),
|
|
91
|
+
destinations: this.containers
|
|
92
|
+
.filter((container) => !container.disabled)
|
|
93
|
+
.map((container) => container.label),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
announcePosition(state) {
|
|
97
|
+
const container = this.findContainer(state.target.containerId);
|
|
98
|
+
if (!container || this.options.itemLabel(state.itemId) === undefined) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.say({
|
|
102
|
+
type: 'position',
|
|
103
|
+
item: this.label(state.itemId),
|
|
104
|
+
container: container.label,
|
|
105
|
+
position: state.target.index + 1,
|
|
106
|
+
count: this.itemsIn(container.id, state.itemId).length + 1,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
moveKeyboardTarget(direction, delta) {
|
|
110
|
+
const drag = this.drag;
|
|
111
|
+
if (drag?.mode !== 'keyboard')
|
|
112
|
+
return;
|
|
113
|
+
const vertical = this.options.orientation?.() === 'vertical';
|
|
114
|
+
const target = { ...drag.target };
|
|
115
|
+
// With same-container reordering off, the source container pins the
|
|
116
|
+
// position (as it does for pointer moves): a vertical step leaves it.
|
|
117
|
+
const pinned = !this.options.allowSameContainerReorder() &&
|
|
118
|
+
target.containerId === drag.source.containerId;
|
|
119
|
+
if (direction === 'vertical') {
|
|
120
|
+
if (pinned && !vertical)
|
|
121
|
+
return;
|
|
122
|
+
const max = this.itemsIn(target.containerId, drag.itemId).length;
|
|
123
|
+
const next = pinned ? (delta < 0 ? -1 : max + 1) : target.index + delta;
|
|
124
|
+
if (vertical && (next < 0 || next > max)) {
|
|
125
|
+
const currentIndex = this.containers.findIndex((container) => container.id === target.containerId);
|
|
126
|
+
const candidate = this.containers[currentIndex + delta];
|
|
127
|
+
if (!candidate)
|
|
128
|
+
return;
|
|
129
|
+
if (candidate.disabled) {
|
|
130
|
+
this.say({ type: 'unavailable', container: candidate.label });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
target.containerId = candidate.id;
|
|
134
|
+
target.index =
|
|
135
|
+
delta < 0 ? this.itemsIn(candidate.id, drag.itemId).length : 0;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
target.index = clamp(next, max);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
if (vertical)
|
|
143
|
+
return;
|
|
144
|
+
const currentIndex = this.containers.findIndex((container) => container.id === target.containerId);
|
|
145
|
+
const candidate = this.containers[currentIndex + delta];
|
|
146
|
+
if (!candidate)
|
|
147
|
+
return;
|
|
148
|
+
if (candidate.disabled) {
|
|
149
|
+
this.say({ type: 'unavailable', container: candidate.label });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
target.containerId = candidate.id;
|
|
153
|
+
target.index = clamp(target.index, this.itemsIn(candidate.id, drag.itemId).length);
|
|
154
|
+
}
|
|
155
|
+
// Coming back into a pinned source container lands on its own slot.
|
|
156
|
+
if (!this.options.allowSameContainerReorder() &&
|
|
157
|
+
target.containerId === drag.source.containerId) {
|
|
158
|
+
target.index = drag.source.index;
|
|
159
|
+
}
|
|
160
|
+
this.drag = { ...drag, target };
|
|
161
|
+
this.announcePosition(this.drag);
|
|
162
|
+
}
|
|
163
|
+
async drop() {
|
|
164
|
+
const state = this.drag;
|
|
165
|
+
if (!state)
|
|
166
|
+
return;
|
|
167
|
+
const itemLabel = this.options.itemLabel(state.itemId);
|
|
168
|
+
const target = this.findContainer(state.target.containerId);
|
|
169
|
+
const valid = itemLabel !== undefined &&
|
|
170
|
+
this.findContainer(state.source.containerId) !== undefined &&
|
|
171
|
+
target !== undefined &&
|
|
172
|
+
!target.disabled;
|
|
173
|
+
this.drag = undefined;
|
|
174
|
+
this.suppressSelection(state.itemId);
|
|
175
|
+
if (!valid || itemLabel === undefined || !target)
|
|
176
|
+
return;
|
|
177
|
+
const changed = state.source.containerId !== state.target.containerId ||
|
|
178
|
+
state.source.index !== state.target.index;
|
|
179
|
+
if (!changed) {
|
|
180
|
+
this.focusItemId = state.itemId;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.pending = true;
|
|
184
|
+
try {
|
|
185
|
+
await this.options.commit({
|
|
186
|
+
itemId: state.itemId,
|
|
187
|
+
source: state.source,
|
|
188
|
+
target: state.target,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
this.say({ type: 'failed', item: itemLabel });
|
|
193
|
+
this.focusItemId = state.itemId;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.pending = false;
|
|
198
|
+
}
|
|
199
|
+
this.say({
|
|
200
|
+
type: 'drop',
|
|
201
|
+
item: itemLabel,
|
|
202
|
+
container: target.label,
|
|
203
|
+
position: state.target.index + 1,
|
|
204
|
+
count: this.itemsIn(target.id, state.itemId).length + 1,
|
|
205
|
+
});
|
|
206
|
+
this.focusItemId = state.itemId;
|
|
207
|
+
}
|
|
208
|
+
/** Cancel the move in flight. Focus returns to the item when `restoreFocus`. */
|
|
209
|
+
cancel(announce = true, restoreFocus = true) {
|
|
210
|
+
const drag = this.drag;
|
|
211
|
+
if (!drag)
|
|
212
|
+
return;
|
|
213
|
+
const label = this.options.itemLabel(drag.itemId);
|
|
214
|
+
if (label !== undefined && announce) {
|
|
215
|
+
this.say({ type: 'cancel', item: label });
|
|
216
|
+
}
|
|
217
|
+
if (label !== undefined && restoreFocus)
|
|
218
|
+
this.focusItemId = drag.itemId;
|
|
219
|
+
this.suppressSelection(drag.itemId);
|
|
220
|
+
this.drag = undefined;
|
|
221
|
+
}
|
|
222
|
+
suppressSelection(itemId) {
|
|
223
|
+
this.suppressedSelectionId = itemId;
|
|
224
|
+
window.setTimeout(() => {
|
|
225
|
+
if (this.suppressedSelectionId === itemId) {
|
|
226
|
+
this.suppressedSelectionId = undefined;
|
|
227
|
+
}
|
|
228
|
+
}, 0);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Keyboard pick-up and native drag gestures can both dispatch a click after
|
|
232
|
+
* their key/pointer sequence. Returns `true` when this click must be
|
|
233
|
+
* consumed so a move never doubles as an application-level activation.
|
|
234
|
+
*/
|
|
235
|
+
consumeClick(itemId) {
|
|
236
|
+
if (this.suppressedSelectionId === itemId) {
|
|
237
|
+
this.suppressedSelectionId = undefined;
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
keydown(event, itemId) {
|
|
243
|
+
const drag = this.drag;
|
|
244
|
+
if (!drag) {
|
|
245
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
246
|
+
if (!this.movable)
|
|
247
|
+
return;
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
this.pickup(itemId, 'keyboard');
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (drag.itemId !== itemId || drag.mode !== 'keyboard')
|
|
254
|
+
return;
|
|
255
|
+
if (event.key === 'Escape') {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
this.cancel();
|
|
258
|
+
}
|
|
259
|
+
else if (event.key === ' ' || event.key === 'Enter') {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
void this.drop();
|
|
262
|
+
}
|
|
263
|
+
else if (event.key === 'ArrowLeft') {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
this.moveKeyboardTarget('horizontal', -1);
|
|
266
|
+
}
|
|
267
|
+
else if (event.key === 'ArrowRight') {
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
this.moveKeyboardTarget('horizontal', 1);
|
|
270
|
+
}
|
|
271
|
+
else if (event.key === 'ArrowUp') {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
this.moveKeyboardTarget('vertical', -1);
|
|
274
|
+
}
|
|
275
|
+
else if (event.key === 'ArrowDown') {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
this.moveKeyboardTarget('vertical', 1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Focus left the item while a keyboard move was in flight: abandon it so
|
|
282
|
+
* the item is not left "picked up" with no way to steer it.
|
|
283
|
+
*/
|
|
284
|
+
blur(event, itemId) {
|
|
285
|
+
const drag = this.drag;
|
|
286
|
+
if (drag?.mode !== 'keyboard' || drag.itemId !== itemId)
|
|
287
|
+
return;
|
|
288
|
+
const next = event.relatedTarget;
|
|
289
|
+
const current = event.currentTarget;
|
|
290
|
+
if (next instanceof Node &&
|
|
291
|
+
current instanceof Node &&
|
|
292
|
+
current.contains(next)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.cancel(true, false);
|
|
296
|
+
}
|
|
297
|
+
dragStart(event, itemId) {
|
|
298
|
+
if (!this.movable) {
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.pickup(itemId, 'pointer');
|
|
303
|
+
event.dataTransfer?.setData('text/plain', itemId);
|
|
304
|
+
if (event.dataTransfer)
|
|
305
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
306
|
+
}
|
|
307
|
+
dragEnd() {
|
|
308
|
+
if (this.drag?.mode === 'pointer')
|
|
309
|
+
this.cancel();
|
|
310
|
+
}
|
|
311
|
+
pointerDown(event, itemId) {
|
|
312
|
+
if (!this.movable || event.button !== 0)
|
|
313
|
+
return;
|
|
314
|
+
this.pointerSession = {
|
|
315
|
+
itemId,
|
|
316
|
+
pointerId: event.pointerId,
|
|
317
|
+
startX: event.clientX,
|
|
318
|
+
startY: event.clientY,
|
|
319
|
+
active: false,
|
|
320
|
+
};
|
|
321
|
+
const target = event.currentTarget;
|
|
322
|
+
target.setPointerCapture?.(event.pointerId);
|
|
323
|
+
}
|
|
324
|
+
elementAtPointer(event) {
|
|
325
|
+
return (document.elementFromPoint?.(event.clientX, event.clientY) ??
|
|
326
|
+
event.target);
|
|
327
|
+
}
|
|
328
|
+
setPointerTargetFromElement(element, clientY) {
|
|
329
|
+
// Container ids belong to the host's domain and may repeat in another
|
|
330
|
+
// instance. Pointer hit-testing must never escape this one's root.
|
|
331
|
+
const root = this.options.root();
|
|
332
|
+
if (!element || !root?.contains(element))
|
|
333
|
+
return undefined;
|
|
334
|
+
const { selectors } = this.options;
|
|
335
|
+
const lane = element.closest(selectors.container);
|
|
336
|
+
const containerId = lane?.dataset[selectors.containerKey];
|
|
337
|
+
if (!containerId)
|
|
338
|
+
return undefined;
|
|
339
|
+
const container = this.findContainer(containerId);
|
|
340
|
+
if (!container || container.disabled)
|
|
341
|
+
return container;
|
|
342
|
+
const itemElement = element.closest(selectors.item);
|
|
343
|
+
if (!itemElement) {
|
|
344
|
+
this.pointerTarget(containerId, this.itemsIn(containerId, this.drag?.itemId).length);
|
|
345
|
+
return container;
|
|
346
|
+
}
|
|
347
|
+
const rawIndex = this.itemsIn(containerId).indexOf(itemElement.dataset[selectors.itemKey] ?? '');
|
|
348
|
+
if (rawIndex < 0)
|
|
349
|
+
return container;
|
|
350
|
+
this.pointerTarget(containerId, this.insertionIndex(containerId, rawIndex, itemElement, clientY));
|
|
351
|
+
return container;
|
|
352
|
+
}
|
|
353
|
+
/** Index to insert at when hovering item `rawIndex`, after removing the dragged item. */
|
|
354
|
+
insertionIndex(containerId, rawIndex, itemElement, clientY) {
|
|
355
|
+
const rect = itemElement.getBoundingClientRect();
|
|
356
|
+
const before = clientY < rect.top + rect.height / 2;
|
|
357
|
+
let targetIndex = rawIndex + (before ? 0 : 1);
|
|
358
|
+
const drag = this.drag;
|
|
359
|
+
if (drag?.source.containerId === containerId &&
|
|
360
|
+
targetIndex > drag.source.index) {
|
|
361
|
+
targetIndex -= 1;
|
|
362
|
+
}
|
|
363
|
+
return targetIndex;
|
|
364
|
+
}
|
|
365
|
+
finishPointerSession(event, cancelled = false) {
|
|
366
|
+
const session = this.pointerSession;
|
|
367
|
+
this.pointerSession = undefined;
|
|
368
|
+
const target = event.currentTarget;
|
|
369
|
+
if (target.hasPointerCapture?.(event.pointerId)) {
|
|
370
|
+
target.releasePointerCapture(event.pointerId);
|
|
371
|
+
}
|
|
372
|
+
if (!session?.active || this.drag?.itemId !== session.itemId)
|
|
373
|
+
return;
|
|
374
|
+
if (cancelled) {
|
|
375
|
+
this.cancel();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const container = this.setPointerTargetFromElement(this.elementAtPointer(event), event.clientY);
|
|
379
|
+
if (container?.disabled) {
|
|
380
|
+
this.say({ type: 'unavailable', container: container.label });
|
|
381
|
+
this.cancel(false);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!container) {
|
|
385
|
+
this.cancel();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
void this.drop();
|
|
389
|
+
}
|
|
390
|
+
pointerUp(event) {
|
|
391
|
+
this.finishPointerSession(event);
|
|
392
|
+
}
|
|
393
|
+
pointerCancel(event) {
|
|
394
|
+
this.finishPointerSession(event, true);
|
|
395
|
+
}
|
|
396
|
+
pointerMove(event) {
|
|
397
|
+
const session = this.pointerSession;
|
|
398
|
+
if (!session || session.pointerId !== event.pointerId)
|
|
399
|
+
return;
|
|
400
|
+
if (!session.active) {
|
|
401
|
+
const distance = Math.hypot(event.clientX - session.startX, event.clientY - session.startY);
|
|
402
|
+
if (distance < 6)
|
|
403
|
+
return;
|
|
404
|
+
this.pickup(session.itemId, 'pointer');
|
|
405
|
+
this.pointerSession = { ...session, active: true };
|
|
406
|
+
}
|
|
407
|
+
event.preventDefault();
|
|
408
|
+
this.setPointerTargetFromElement(this.elementAtPointer(event), event.clientY);
|
|
409
|
+
}
|
|
410
|
+
pointerTarget(containerId, index) {
|
|
411
|
+
const drag = this.drag;
|
|
412
|
+
if (drag?.mode !== 'pointer')
|
|
413
|
+
return;
|
|
414
|
+
if (!this.options.allowSameContainerReorder() &&
|
|
415
|
+
containerId === drag.source.containerId) {
|
|
416
|
+
this.drag = { ...drag, target: { ...drag.source } };
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const max = this.itemsIn(containerId, drag.itemId).length;
|
|
420
|
+
this.drag = { ...drag, target: { containerId, index: clamp(index, max) } };
|
|
421
|
+
}
|
|
422
|
+
/** Native drop on an item (`rawIndex` is its index in its container). */
|
|
423
|
+
dropOnItem(event, containerId, rawIndex) {
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
const drag = this.drag;
|
|
426
|
+
if (drag?.mode !== 'pointer')
|
|
427
|
+
return;
|
|
428
|
+
const container = this.findContainer(containerId);
|
|
429
|
+
if (container?.disabled) {
|
|
430
|
+
this.say({ type: 'unavailable', container: container.label });
|
|
431
|
+
this.cancel(false);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.pointerTarget(containerId, this.insertionIndex(containerId, rawIndex, event.currentTarget, event.clientY));
|
|
435
|
+
void this.drop();
|
|
436
|
+
}
|
|
437
|
+
/** Native drop on a container's empty space: append. */
|
|
438
|
+
dropOnContainer(event, containerId) {
|
|
439
|
+
event.preventDefault();
|
|
440
|
+
const container = this.findContainer(containerId);
|
|
441
|
+
if (container?.disabled) {
|
|
442
|
+
this.say({ type: 'unavailable', container: container.label });
|
|
443
|
+
this.cancel(false);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.pointerTarget(containerId, this.itemsIn(containerId, this.drag?.itemId).length);
|
|
447
|
+
void this.drop();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Create a controller; call during component initialization. */
|
|
451
|
+
export function createSortable(options) {
|
|
452
|
+
return new SortableController(options);
|
|
453
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { SortableAnnouncement, SortableContainerInfo, SortableControllerOptions, SortableMove, SortableSelectors, } from './controller.svelte.js';
|
|
2
|
+
export { createSortable, SortableController } from './controller.svelte.js';
|
|
3
|
+
export { default as Sortable } from './Sortable.svelte';
|
|
4
|
+
export type { SortableContainer, SortableContainerHeaderSnippetProps, SortableContainerMove, SortableItem, SortableItemMove, SortableItemSnippetProps, SortablePosition, SortableProps, } from './types.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/sortable/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,YAAY,EACZ,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,YAAY,EACV,iBAAiB,EACjB,mCAAmC,EACnC,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,gBAAgB,EAChB,aAAa,GACd,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { SortablePosition } from './controller.svelte.js';
|
|
3
|
+
export type { SortablePosition } from './controller.svelte.js';
|
|
4
|
+
/** The minimum identity contract for a sortable item. */
|
|
5
|
+
export interface SortableItem {
|
|
6
|
+
id: string;
|
|
7
|
+
/** The container the item currently sits in. */
|
|
8
|
+
containerId: string;
|
|
9
|
+
/** Accessible text used in announcements and the move handle's name. */
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
/** A list items are sorted within and moved between. */
|
|
13
|
+
export interface SortableContainer {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
/** Items cannot be dropped into a disabled container. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Keeps the container where it is when `reorderContainers` is on: fixed
|
|
20
|
+
* containers render first, in the order given, and have no move handle.
|
|
21
|
+
*/
|
|
22
|
+
fixed?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** A drop of an item. `target.index` counts after removing it from `source`. */
|
|
25
|
+
export interface SortableItemMove<Item extends SortableItem = SortableItem, Container extends SortableContainer = SortableContainer> {
|
|
26
|
+
item: Item;
|
|
27
|
+
source: SortablePosition;
|
|
28
|
+
target: SortablePosition;
|
|
29
|
+
sourceContainer: Container;
|
|
30
|
+
targetContainer: Container;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A drop of a container. Indices count the movable (non-`fixed`) containers;
|
|
34
|
+
* `target` counts after removing the container from `source`.
|
|
35
|
+
*/
|
|
36
|
+
export interface SortableContainerMove<Container extends SortableContainer = SortableContainer> {
|
|
37
|
+
container: Container;
|
|
38
|
+
source: number;
|
|
39
|
+
target: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SortableItemSnippetProps<Item extends SortableItem, Container extends SortableContainer> {
|
|
42
|
+
item: Item;
|
|
43
|
+
container: Container;
|
|
44
|
+
/** Index within the container. */
|
|
45
|
+
index: number;
|
|
46
|
+
isDragging: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface SortableContainerHeaderSnippetProps<Container extends SortableContainer> {
|
|
49
|
+
container: Container;
|
|
50
|
+
count: number;
|
|
51
|
+
isDragging: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Public props for the generic Svelte 5 Sortable component. */
|
|
54
|
+
export interface SortableProps<Item extends SortableItem, Container extends SortableContainer> {
|
|
55
|
+
/** Containers in display order. The owner is authoritative. */
|
|
56
|
+
containers: readonly Container[];
|
|
57
|
+
/** Items; order within a container is their order here. */
|
|
58
|
+
items: readonly Item[];
|
|
59
|
+
/** Accessible name for the whole list. */
|
|
60
|
+
label?: string;
|
|
61
|
+
/** Let the user reorder the (non-`fixed`) containers too. */
|
|
62
|
+
reorderContainers?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether items may be reordered within their container (default true);
|
|
65
|
+
* `false` only allows moving them between containers.
|
|
66
|
+
*/
|
|
67
|
+
allowSameContainerReorder?: boolean;
|
|
68
|
+
/** Content beside each item's move handle. */
|
|
69
|
+
item: Snippet<[SortableItemSnippetProps<Item, Container>]>;
|
|
70
|
+
/** Content in a container's header, beside its move handle. */
|
|
71
|
+
containerHeader?: Snippet<[SortableContainerHeaderSnippetProps<Container>]>;
|
|
72
|
+
/**
|
|
73
|
+
* Receives a typed item move. May persist asynchronously; a rejection
|
|
74
|
+
* restores the presentation and is announced. Omit to make items read-only.
|
|
75
|
+
*/
|
|
76
|
+
onmove?: (move: SortableItemMove<Item, Container>) => void | Promise<void>;
|
|
77
|
+
/** Receives a container move (requires `reorderContainers`). */
|
|
78
|
+
oncontainermove?: (move: SortableContainerMove<Container>) => void | Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/sortable/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB,CAC/B,IAAI,SAAS,YAAY,GAAG,YAAY,EACxC,SAAS,SAAS,iBAAiB,GAAG,iBAAiB;IAEvD,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,eAAe,EAAE,SAAS,CAAC;IAC3B,eAAe,EAAE,SAAS,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB,CACpC,SAAS,SAAS,iBAAiB,GAAG,iBAAiB;IAEvD,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB,CACvC,IAAI,SAAS,YAAY,EACzB,SAAS,SAAS,iBAAiB;IAEnC,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,SAAS,CAAC;IACrB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,mCAAmC,CAClD,SAAS,SAAS,iBAAiB;IAEnC,SAAS,EAAE,SAAS,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa,CAC5B,IAAI,SAAS,YAAY,EACzB,SAAS,SAAS,iBAAiB;IAEnC,+DAA+D;IAC/D,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IACjC,2DAA2D;IAC3D,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,8CAA8C;IAC9C,IAAI,EAAE,OAAO,CAAC,CAAC,wBAAwB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAC3D,+DAA+D;IAC/D,eAAe,CAAC,EAAE,OAAO,CAAC,CAAC,mCAAmC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC5E;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,gEAAgE;IAChE,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,qBAAqB,CAAC,SAAS,CAAC,KACnC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -116,6 +116,11 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
116
116
|
* Phone-only context bar over the top of the main region. Hides while
|
|
117
117
|
* scrolling down and returns on the first scroll up (see
|
|
118
118
|
* `phone.hideOnScroll`); pinned while `pinChrome` or a drawer is open.
|
|
119
|
+
* Mark an actual replacement title with `data-shell-page-title-replacement`
|
|
120
|
+
* to visually hide the corresponding PageHeader title. Action-only or
|
|
121
|
+
* workspace-name bars leave the page heading visible. Mark replacement
|
|
122
|
+
* back navigation with `data-shell-page-navigation-replacement` to hide
|
|
123
|
+
* PageHeader breadcrumbs; a title alone does not replace navigation.
|
|
119
124
|
*/
|
|
120
125
|
phoneTopBar?: Snippet<[{ hidden: boolean }]>;
|
|
121
126
|
/**
|
|
@@ -344,6 +349,53 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
344
349
|
});
|
|
345
350
|
});
|
|
346
351
|
|
|
352
|
+
// Dock opened from code (`useShellDock`, header dock toggles): focus moves
|
|
353
|
+
// into the dock once it is shown.
|
|
354
|
+
let lastDockFocusRequest = 0;
|
|
355
|
+
$effect(() => {
|
|
356
|
+
const request = shell.dockFocusRequest;
|
|
357
|
+
untrack(() => {
|
|
358
|
+
if (request === lastDockFocusRequest) return;
|
|
359
|
+
lastDockFocusRequest = request;
|
|
360
|
+
void tick().then(focusDock);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
function focusDock(): void {
|
|
365
|
+
const aside = sideElements.right;
|
|
366
|
+
const panel = aside?.querySelector<HTMLElement>(
|
|
367
|
+
'.smrt-admin-shell__panel--right',
|
|
368
|
+
);
|
|
369
|
+
if (!panel || panel.hidden || !edgeExpanded('right')) return;
|
|
370
|
+
if (panel.contains(document.activeElement)) return;
|
|
371
|
+
const target =
|
|
372
|
+
panel.querySelector<HTMLElement>(
|
|
373
|
+
'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])',
|
|
374
|
+
) ?? panel;
|
|
375
|
+
target.focus({ preventScroll: true });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// When the dock closes (Escape, a toggle, or code), focus goes back to
|
|
379
|
+
// where it was opened from if it was left inside the dock or lost.
|
|
380
|
+
let dockWasOpen = false;
|
|
381
|
+
$effect(() => {
|
|
382
|
+
const open = edgeExpanded('right');
|
|
383
|
+
untrack(() => {
|
|
384
|
+
const wasOpen = dockWasOpen;
|
|
385
|
+
dockWasOpen = open;
|
|
386
|
+
if (!wasOpen || open) return;
|
|
387
|
+
const target = shell.dockReturnFocus;
|
|
388
|
+
shell.dockReturnFocus = null;
|
|
389
|
+
if (!target?.isConnected || typeof document === 'undefined') return;
|
|
390
|
+
const active = document.activeElement;
|
|
391
|
+
const lost =
|
|
392
|
+
!active ||
|
|
393
|
+
active === document.body ||
|
|
394
|
+
Boolean(sideElements.right?.contains(active));
|
|
395
|
+
if (lost) target.focus({ preventScroll: true });
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
347
399
|
function onEdgeAnimationEnd(event: AnimationEvent, edge: SideEdge): void {
|
|
348
400
|
if (closingEdge === edge && event.target === event.currentTarget) {
|
|
349
401
|
endClosing();
|
|
@@ -772,7 +824,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
772
824
|
{@render shellBrand()}
|
|
773
825
|
{@render edgeToggle('top')}
|
|
774
826
|
{/if}
|
|
775
|
-
{#if shell.config.panels.left.initial !== 'hidden' && (!isPhone || shell.phonePresentation('left') !== 'hidden')}
|
|
827
|
+
{#if shell.config.panels.left.initial !== 'hidden' && shell.layoutPanels.left?.visible !== false && (!isPhone || shell.phonePresentation('left') !== 'hidden')}
|
|
776
828
|
<div class="smrt-admin-shell__tenant-opener" class:restore-hidden={panelState('left') === 'hidden'}>
|
|
777
829
|
<Button variant="ghost" size="sm" aria-label={t(M['ui.admin_shell.menu'])} aria-expanded={edgeExpanded('left')} aria-controls="smrt-admin-shell-left-panel" onclick={() => shell.setPanelState('left', edgeExpanded('left') ? 'collapsed' : 'expanded')}>{t(M['ui.admin_shell.menu'])}</Button>
|
|
778
830
|
</div>
|
|
@@ -983,6 +1035,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
983
1035
|
{#if panelMounted('right')}
|
|
984
1036
|
<div
|
|
985
1037
|
class="smrt-admin-shell__panel smrt-admin-shell__panel--right"
|
|
1038
|
+
tabindex="-1"
|
|
986
1039
|
hidden={!shownOpen('right')}
|
|
987
1040
|
>
|
|
988
1041
|
{#key shell.activeFocusToolId}
|
|
@@ -1252,6 +1305,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
1252
1305
|
justify-content: space-between;
|
|
1253
1306
|
gap: var(--smrt-spacing-3);
|
|
1254
1307
|
min-width: 0;
|
|
1308
|
+
min-block-size: 0;
|
|
1255
1309
|
padding: 0 var(--smrt-spacing-4);
|
|
1256
1310
|
}
|
|
1257
1311
|
|
|
@@ -1584,6 +1638,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
1584
1638
|
align-self: start;
|
|
1585
1639
|
z-index: 10;
|
|
1586
1640
|
min-inline-size: 0;
|
|
1641
|
+
background: var(--smrt-color-surface);
|
|
1587
1642
|
transition: transform var(--smrt-admin-shell-chrome-duration)
|
|
1588
1643
|
var(--smrt-easing-standard, ease);
|
|
1589
1644
|
}
|
|
@@ -1750,11 +1805,15 @@ function buildLayoutStyle(shell: ModuleShellState): string {
|
|
|
1750
1805
|
the page title (visually hidden: screen readers and the outline keep
|
|
1751
1806
|
it); page tabs stick under it and slide away with it; a form's action
|
|
1752
1807
|
row is fixed to the bottom and replaces the bottom bar. */
|
|
1753
|
-
.smrt-admin-shell[data-
|
|
1808
|
+
.smrt-admin-shell[data-phone-top]:has(
|
|
1809
|
+
> .smrt-admin-shell__phone-top :global([data-shell-page-navigation-replacement])
|
|
1810
|
+
) :global([data-shell-breadcrumbs]) {
|
|
1754
1811
|
display: none;
|
|
1755
1812
|
}
|
|
1756
1813
|
|
|
1757
|
-
.smrt-admin-shell[data-phone-top]
|
|
1814
|
+
.smrt-admin-shell[data-phone-top]:has(
|
|
1815
|
+
> .smrt-admin-shell__phone-top :global([data-shell-page-title-replacement])
|
|
1816
|
+
) :global([data-shell-page-title]) {
|
|
1758
1817
|
position: absolute;
|
|
1759
1818
|
inline-size: 1px;
|
|
1760
1819
|
block-size: 1px;
|
|
@@ -52,6 +52,11 @@ interface Props extends AdminShellProps {
|
|
|
52
52
|
* Phone-only context bar over the top of the main region. Hides while
|
|
53
53
|
* scrolling down and returns on the first scroll up (see
|
|
54
54
|
* `phone.hideOnScroll`); pinned while `pinChrome` or a drawer is open.
|
|
55
|
+
* Mark an actual replacement title with `data-shell-page-title-replacement`
|
|
56
|
+
* to visually hide the corresponding PageHeader title. Action-only or
|
|
57
|
+
* workspace-name bars leave the page heading visible. Mark replacement
|
|
58
|
+
* back navigation with `data-shell-page-navigation-replacement` to hide
|
|
59
|
+
* PageHeader breadcrumbs; a title alone does not replace navigation.
|
|
55
60
|
*/
|
|
56
61
|
phoneTopBar?: Snippet<[{
|
|
57
62
|
hidden: boolean;
|