dsh-sidebar-organizer 0.4.1 → 0.4.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/CHANGELOG.md +23 -0
- package/README.en.md +2 -2
- package/README.md +3 -2
- package/lib/client.js +172 -7
- package/package.json +1 -1
- package/src/runtime.js +128 -4
- package/src/styles.js +42 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
版本格式遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
本文件的中文条目为默认语言;英文条目见英文段落(同一版本内)。
|
|
5
5
|
|
|
6
|
+
## 0.4.2 — 2026-11-02
|
|
7
|
+
|
|
8
|
+
- 拖动到「更多」按钮上时新增收纳动画:按钮呼吸高亮、图标下沉,拖影微微缩小
|
|
9
|
+
变淡;松手后拖影飞向按钮并缩小消失,随后该行被收纳,按钮播放一次「吞入」
|
|
10
|
+
回弹。只剩最后一行可见时不再静默失败,改为按钮左右轻抖提示不可收纳。
|
|
11
|
+
- 动画只使用 `transform` / `opacity`(拖影飞入用 WAAPI,其余为 CSS),拖动热路
|
|
12
|
+
径不新增布局读取;同一时刻只允许一个飞入动画,连续快速收纳不会叠加拖影。
|
|
13
|
+
- 尊重 `prefers-reduced-motion: reduce`:关闭呼吸、回弹与抖动,飞入改为立即
|
|
14
|
+
收纳。
|
|
15
|
+
|
|
16
|
+
### English
|
|
17
|
+
|
|
18
|
+
- Added the tuck-away animation on the “More” anchor: the button breathes, its
|
|
19
|
+
glyph dips, and the carried ghost shrinks slightly while armed; on release the
|
|
20
|
+
ghost flies into the anchor and fades, the row is tucked away, and the button
|
|
21
|
+
plays a short “swallow” bounce. When only one row is visible the anchor shakes
|
|
22
|
+
instead of silently refusing to hide it.
|
|
23
|
+
- Only `transform` / `opacity` animate (the flight uses WAAPI, the rest is CSS),
|
|
24
|
+
the drag hot path gains no layout reads, and a single flight runs at a time so
|
|
25
|
+
rapid drops cannot pile ghosts up.
|
|
26
|
+
- Honors `prefers-reduced-motion: reduce`: no breathing, bounce or shake, and the
|
|
27
|
+
row is tucked away immediately.
|
|
28
|
+
|
|
6
29
|
## 0.4.1 — 2026-11-02
|
|
7
30
|
|
|
8
31
|
- 修复反复拖动时排序“抽搐”:落点改为按当前视觉顺序(`layout.order` +
|
package/README.en.md
CHANGED
|
@@ -6,8 +6,8 @@ Adds **custom ordering** and **tucking away** to the "global panels" strip at th
|
|
|
6
6
|
top-left of the DSH (DeepSeek Harness) sidebar — the column of icons for plugins,
|
|
7
7
|
marketplace, skills manager, scheduled tasks, and so on.
|
|
8
8
|
|
|
9
|
-
> Version **0.4.
|
|
10
|
-
>
|
|
9
|
+
> Version **0.4.2**: adds the tuck-away animation when dragging onto “More”
|
|
10
|
+
> (breathing highlight, ghost flight, receive bounce).
|
|
11
11
|
|
|
12
12
|
## What it does
|
|
13
13
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
给 DSH(DeepSeek Harness)左侧栏左上角那块「全局面板」栏——插件、市场、技能管理、定时任务……那一列图标——加上**自定义排序**与**收纳**能力。
|
|
6
6
|
|
|
7
|
-
> 当前版本 **0.4.
|
|
7
|
+
> 当前版本 **0.4.2**:拖动到「更多」时新增收纳动画(按钮呼吸高亮、拖影飞入收纳)。
|
|
8
8
|
|
|
9
9
|
## 现在能做什么
|
|
10
10
|
|
|
@@ -132,8 +132,9 @@ npm run check # 语法检查
|
|
|
132
132
|
- [x] 0.3.4 — 图标内嵌 id 加前缀;「已收纳」内排序
|
|
133
133
|
- [x] 0.3.5 — 指针捕获延后到拖动成立,修复收纳条目无法点击
|
|
134
134
|
- [x] 0.3.6 — 点击收纳条目只打开面板,不再自动放回侧栏
|
|
135
|
-
- [x] 0.4.0 — 源码模块化与构建脚本、多语言文档、AI 使用与代码来源声明
|
|
135
|
+
- [x] 0.4.0 — 源码模块化与构建脚本、多语言文档、AI 使用与代码来源声明
|
|
136
136
|
- [x] 0.4.1 — 修复反复拖动时的排序抖动与「更多」误收纳
|
|
137
|
+
- [x] 0.4.2 — 拖动到「更多」的收纳动画(呼吸高亮、拖影飞入、回弹与拒绝抖动)
|
|
137
138
|
- [ ] 0.5.0 — 无障碍补强(菜单内上移 / 下移按钮、屏幕阅读器朗读顺序变化)
|
|
138
139
|
|
|
139
140
|
## 声明与引用
|
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-sidebar-organizer — client half (v0.4.
|
|
2
|
+
* dsh-sidebar-organizer — client half (v0.4.2).
|
|
3
3
|
*
|
|
4
4
|
* GENERATED FILE — do not edit by hand.
|
|
5
5
|
* Sources live in src/; run `npm run build` to regenerate.
|
|
@@ -15,7 +15,7 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
/* Constants, locale strings and the module-level shared handles. */
|
|
16
16
|
|
|
17
17
|
// Replaced at build time from package.json by scripts/build.mjs.
|
|
18
|
-
const VERSION = "0.4.
|
|
18
|
+
const VERSION = "0.4.2";
|
|
19
19
|
|
|
20
20
|
const PANEL_SLOT = "sidebar.panellist";
|
|
21
21
|
const ACTIONS_SLOT = "sidebar.panellist.actions";
|
|
@@ -92,10 +92,51 @@ const CSS = `
|
|
|
92
92
|
button[data-dso-key].dso-dragging { opacity: 0.35; }
|
|
93
93
|
button[data-dso-key].dso-pressing { transform: scale(0.94); transition: transform 120ms ease; }
|
|
94
94
|
button[data-dso-key] { -webkit-user-select: none; user-select: none; }
|
|
95
|
-
|
|
95
|
+
/* The "More" anchor, armed: it breathes a ring and dips its glyph to read as
|
|
96
|
+
"release here and this row drops into me". */
|
|
97
|
+
button.dso-drop-more {
|
|
96
98
|
outline: 2px dashed var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
97
99
|
outline-offset: -2px;
|
|
98
100
|
background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08));
|
|
101
|
+
animation: dso-more-breathe 1100ms ease-in-out infinite;
|
|
102
|
+
}
|
|
103
|
+
button.dso-drop-more span[aria-hidden="true"] {
|
|
104
|
+
transform: translateY(2px) scale(0.88);
|
|
105
|
+
transition: transform 160ms ease;
|
|
106
|
+
}
|
|
107
|
+
@keyframes dso-more-breathe {
|
|
108
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(77, 107, 254, 0.38); }
|
|
109
|
+
55% { box-shadow: 0 0 0 8px rgba(77, 107, 254, 0); }
|
|
110
|
+
100% { box-shadow: 0 0 0 0 rgba(77, 107, 254, 0); }
|
|
111
|
+
}
|
|
112
|
+
@keyframes dso-more-receive {
|
|
113
|
+
0% { transform: scale(1); }
|
|
114
|
+
35% { transform: scale(0.86); }
|
|
115
|
+
70% { transform: scale(1.06); }
|
|
116
|
+
100% { transform: scale(1); }
|
|
117
|
+
}
|
|
118
|
+
button.dso-drop-receive { animation: dso-more-receive 260ms cubic-bezier(0.2, 0.7, 0.3, 1); }
|
|
119
|
+
@keyframes dso-drop-reject {
|
|
120
|
+
0%, 100% { transform: translateX(0); }
|
|
121
|
+
20% { transform: translateX(-4px); }
|
|
122
|
+
40% { transform: translateX(4px); }
|
|
123
|
+
60% { transform: translateX(-3px); }
|
|
124
|
+
80% { transform: translateX(2px); }
|
|
125
|
+
}
|
|
126
|
+
button.dso-drop-reject { animation: dso-drop-reject 240ms ease; }
|
|
127
|
+
/* Carrying a row towards the anchor: the ghost dims and shrinks a little, so
|
|
128
|
+
its eventual absorption does not feel like a jump. */
|
|
129
|
+
.dso-ghost.dso-ghost-armed {
|
|
130
|
+
opacity: 0.82;
|
|
131
|
+
transform: scale(0.94);
|
|
132
|
+
transition: transform 140ms ease, opacity 140ms ease;
|
|
133
|
+
}
|
|
134
|
+
@media (prefers-reduced-motion: reduce) {
|
|
135
|
+
button.dso-drop-more,
|
|
136
|
+
button.dso-drop-receive,
|
|
137
|
+
button.dso-drop-reject { animation: none !important; }
|
|
138
|
+
.dso-ghost,
|
|
139
|
+
.dso-ghost.dso-ghost-armed { transition: none !important; }
|
|
99
140
|
}
|
|
100
141
|
body.dso-dragging,
|
|
101
142
|
body.dso-dragging * { cursor: grabbing !important; }
|
|
@@ -312,6 +353,7 @@ function createRuntime(ctx) {
|
|
|
312
353
|
passDeferred: false,
|
|
313
354
|
menuListeners: new Set(),
|
|
314
355
|
dismissMenu: null,
|
|
356
|
+
absorbing: null,
|
|
315
357
|
};
|
|
316
358
|
|
|
317
359
|
function log(...args) {
|
|
@@ -694,9 +736,10 @@ function createRuntime(ctx) {
|
|
|
694
736
|
|
|
695
737
|
/* --- drag gesture ------------------------------------------------ */
|
|
696
738
|
|
|
697
|
-
function clearGesture() {
|
|
739
|
+
function clearGesture(options) {
|
|
698
740
|
const gesture = state.gesture;
|
|
699
741
|
if (!gesture) return;
|
|
742
|
+
const keepGhost = Boolean(options && options.keepGhost);
|
|
700
743
|
state.gesture = null;
|
|
701
744
|
if (gesture.timer) window.clearTimeout(gesture.timer);
|
|
702
745
|
if (gesture.pressTimer) window.clearTimeout(gesture.pressTimer);
|
|
@@ -717,7 +760,11 @@ function createRuntime(ctx) {
|
|
|
717
760
|
/* the element may already be detached */
|
|
718
761
|
}
|
|
719
762
|
}
|
|
720
|
-
if (gesture.ghost
|
|
763
|
+
if (gesture.ghost) {
|
|
764
|
+
gesture.ghost.classList.remove("dso-ghost-armed");
|
|
765
|
+
// A drop on the "More" anchor keeps the ghost alive for the absorb flight.
|
|
766
|
+
if (!keepGhost && gesture.ghost.parentNode) gesture.ghost.remove();
|
|
767
|
+
}
|
|
721
768
|
if (gesture.indicator && gesture.indicator.parentNode) gesture.indicator.remove();
|
|
722
769
|
if (gesture.element) {
|
|
723
770
|
gesture.element.classList.remove("dso-dragging");
|
|
@@ -937,6 +984,96 @@ function createRuntime(ctx) {
|
|
|
937
984
|
);
|
|
938
985
|
}
|
|
939
986
|
|
|
987
|
+
function prefersReducedMotion() {
|
|
988
|
+
try {
|
|
989
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
990
|
+
} catch (error) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** Replay a one-shot CSS animation class, even if it is already on. */
|
|
996
|
+
function pulseClass(element, className, duration) {
|
|
997
|
+
if (!element) return;
|
|
998
|
+
element.classList.remove(className);
|
|
999
|
+
// force a style flush so a repeated pulse restarts instead of being ignored
|
|
1000
|
+
void element.offsetWidth;
|
|
1001
|
+
element.classList.add(className);
|
|
1002
|
+
window.setTimeout(() => element.classList.remove(className), duration);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** The carried row leans towards the anchor: dim and shrink the ghost. */
|
|
1006
|
+
function setGhostArmed(gesture, armed) {
|
|
1007
|
+
const ghost = gesture.ghost;
|
|
1008
|
+
if (!ghost || !ghost.isConnected) return;
|
|
1009
|
+
if (ghost.classList.contains("dso-ghost-armed") === armed) return;
|
|
1010
|
+
ghost.classList.toggle("dso-ghost-armed", armed);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Fly the carried ghost into the "More" anchor, then tuck the row away.
|
|
1015
|
+
* Only transform/opacity animate, and only one flight runs at a time; a drop
|
|
1016
|
+
* that arrives mid-flight lands immediately so ghosts can never pile up.
|
|
1017
|
+
*/
|
|
1018
|
+
function absorbIntoMore(gesture) {
|
|
1019
|
+
const moreButton =
|
|
1020
|
+
gesture.moreButton && gesture.moreButton.isConnected ? gesture.moreButton : null;
|
|
1021
|
+
const ghost = gesture.ghost;
|
|
1022
|
+
if (!ghost || !ghost.isConnected || !moreButton || prefersReducedMotion() || state.absorbing) {
|
|
1023
|
+
if (moreButton) pulseClass(moreButton, "dso-drop-receive", 260);
|
|
1024
|
+
if (ghost && ghost.parentNode) ghost.remove();
|
|
1025
|
+
if (alive()) hideKey(gesture.key);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const ghostRect = ghost.getBoundingClientRect();
|
|
1030
|
+
const moreRect = moreButton.getBoundingClientRect();
|
|
1031
|
+
const dx = moreRect.left + moreRect.width / 2 - (ghostRect.left + ghostRect.width / 2);
|
|
1032
|
+
const dy = moreRect.top + moreRect.height / 2 - (ghostRect.top + ghostRect.height / 2);
|
|
1033
|
+
|
|
1034
|
+
let animation = null;
|
|
1035
|
+
try {
|
|
1036
|
+
animation = ghost.animate(
|
|
1037
|
+
[
|
|
1038
|
+
{ transform: "translate(0px, 0px) scale(1)", opacity: 0.96, offset: 0 },
|
|
1039
|
+
{
|
|
1040
|
+
transform: `translate(${dx * 0.55}px, ${dy * 0.55}px) scale(0.7)`,
|
|
1041
|
+
opacity: 0.9,
|
|
1042
|
+
offset: 0.55,
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
transform: `translate(${dx}px, ${dy}px) scale(0.12)`,
|
|
1046
|
+
opacity: 0,
|
|
1047
|
+
offset: 1,
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
{ duration: 180, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" }
|
|
1051
|
+
);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
animation = null;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
pulseClass(moreButton, "dso-drop-receive", 260);
|
|
1057
|
+
|
|
1058
|
+
if (!animation) {
|
|
1059
|
+
if (ghost.parentNode) ghost.remove();
|
|
1060
|
+
if (alive()) hideKey(gesture.key);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
state.absorbing = animation;
|
|
1065
|
+
const finish = () => {
|
|
1066
|
+
if (state.absorbing === animation) state.absorbing = null;
|
|
1067
|
+
if (ghost.parentNode) ghost.remove();
|
|
1068
|
+
if (alive()) hideKey(gesture.key);
|
|
1069
|
+
};
|
|
1070
|
+
if (animation.finished && typeof animation.finished.then === "function") {
|
|
1071
|
+
animation.finished.then(finish, finish);
|
|
1072
|
+
} else {
|
|
1073
|
+
window.setTimeout(finish, 200);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
940
1077
|
function updateTarget(gesture) {
|
|
941
1078
|
const indicator = gesture.indicator;
|
|
942
1079
|
if (!indicator) return;
|
|
@@ -998,6 +1135,7 @@ function createRuntime(ctx) {
|
|
|
998
1135
|
gesture.moreButton = snapshot.moreButton;
|
|
999
1136
|
if (gesture.moreButton) gesture.moreButton.classList.add("dso-drop-more");
|
|
1000
1137
|
}
|
|
1138
|
+
setGhostArmed(gesture, true);
|
|
1001
1139
|
if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
|
|
1002
1140
|
gesture.target = { kind: "hide" };
|
|
1003
1141
|
return;
|
|
@@ -1007,6 +1145,7 @@ function createRuntime(ctx) {
|
|
|
1007
1145
|
gesture.moreButton.classList.remove("dso-drop-more");
|
|
1008
1146
|
gesture.moreButton = null;
|
|
1009
1147
|
}
|
|
1148
|
+
setGhostArmed(gesture, false);
|
|
1010
1149
|
|
|
1011
1150
|
if (!insideRect(snapshot.navRect, x, y, 24) || others.length === 0) {
|
|
1012
1151
|
if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
|
|
@@ -1055,7 +1194,15 @@ function createRuntime(ctx) {
|
|
|
1055
1194
|
const { key, target, source } = gesture;
|
|
1056
1195
|
if (!target) return;
|
|
1057
1196
|
if (target.kind === "hide") {
|
|
1058
|
-
hideKey
|
|
1197
|
+
// Last-visible-row protection lives in hideKey; bounce the anchor instead
|
|
1198
|
+
// of starting an absorb flight that would end in nothing.
|
|
1199
|
+
const { rows } = visibleRows();
|
|
1200
|
+
const visible = visibleKeys(rows);
|
|
1201
|
+
if (!visible.includes(key) || visible.length <= 1) {
|
|
1202
|
+
pulseClass(gesture.moreButton, "dso-drop-reject", 240);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
absorbIntoMore(gesture);
|
|
1059
1206
|
return;
|
|
1060
1207
|
}
|
|
1061
1208
|
if (target.kind === "place-menu") {
|
|
@@ -1115,7 +1262,10 @@ function createRuntime(ctx) {
|
|
|
1115
1262
|
if (!gesture) return;
|
|
1116
1263
|
if (gesture.phase === "dragging") {
|
|
1117
1264
|
const pending = gesture;
|
|
1118
|
-
|
|
1265
|
+
// A drop on the anchor keeps the ghost for the absorb flight; every other
|
|
1266
|
+
// drop tears the drag down first so commits never see stale classes.
|
|
1267
|
+
const absorbing = Boolean(pending.target && pending.target.kind === "hide");
|
|
1268
|
+
clearGesture({ keepGhost: absorbing });
|
|
1119
1269
|
commitDrag(pending);
|
|
1120
1270
|
state.guardUntil = Date.now() + CLICK_GUARD_MS;
|
|
1121
1271
|
return;
|
|
@@ -1254,6 +1404,21 @@ function createRuntime(ctx) {
|
|
|
1254
1404
|
function stop() {
|
|
1255
1405
|
state.stopped = true;
|
|
1256
1406
|
clearGesture();
|
|
1407
|
+
if (state.absorbing) {
|
|
1408
|
+
try {
|
|
1409
|
+
state.absorbing.cancel();
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
/* ignore */
|
|
1412
|
+
}
|
|
1413
|
+
state.absorbing = null;
|
|
1414
|
+
}
|
|
1415
|
+
// the absorb flight may have left a ghost or a one-shot class behind
|
|
1416
|
+
for (const ghost of document.querySelectorAll(".dso-ghost")) ghost.remove();
|
|
1417
|
+
for (const node of document.querySelectorAll(
|
|
1418
|
+
".dso-drop-more, .dso-drop-receive, .dso-drop-reject"
|
|
1419
|
+
)) {
|
|
1420
|
+
node.classList.remove("dso-drop-more", "dso-drop-receive", "dso-drop-reject");
|
|
1421
|
+
}
|
|
1257
1422
|
if (state.pollTimer) {
|
|
1258
1423
|
window.clearInterval(state.pollTimer);
|
|
1259
1424
|
state.pollTimer = 0;
|
package/package.json
CHANGED
package/src/runtime.js
CHANGED
|
@@ -17,6 +17,7 @@ function createRuntime(ctx) {
|
|
|
17
17
|
passDeferred: false,
|
|
18
18
|
menuListeners: new Set(),
|
|
19
19
|
dismissMenu: null,
|
|
20
|
+
absorbing: null,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
function log(...args) {
|
|
@@ -399,9 +400,10 @@ function createRuntime(ctx) {
|
|
|
399
400
|
|
|
400
401
|
/* --- drag gesture ------------------------------------------------ */
|
|
401
402
|
|
|
402
|
-
function clearGesture() {
|
|
403
|
+
function clearGesture(options) {
|
|
403
404
|
const gesture = state.gesture;
|
|
404
405
|
if (!gesture) return;
|
|
406
|
+
const keepGhost = Boolean(options && options.keepGhost);
|
|
405
407
|
state.gesture = null;
|
|
406
408
|
if (gesture.timer) window.clearTimeout(gesture.timer);
|
|
407
409
|
if (gesture.pressTimer) window.clearTimeout(gesture.pressTimer);
|
|
@@ -422,7 +424,11 @@ function createRuntime(ctx) {
|
|
|
422
424
|
/* the element may already be detached */
|
|
423
425
|
}
|
|
424
426
|
}
|
|
425
|
-
if (gesture.ghost
|
|
427
|
+
if (gesture.ghost) {
|
|
428
|
+
gesture.ghost.classList.remove("dso-ghost-armed");
|
|
429
|
+
// A drop on the "More" anchor keeps the ghost alive for the absorb flight.
|
|
430
|
+
if (!keepGhost && gesture.ghost.parentNode) gesture.ghost.remove();
|
|
431
|
+
}
|
|
426
432
|
if (gesture.indicator && gesture.indicator.parentNode) gesture.indicator.remove();
|
|
427
433
|
if (gesture.element) {
|
|
428
434
|
gesture.element.classList.remove("dso-dragging");
|
|
@@ -642,6 +648,96 @@ function createRuntime(ctx) {
|
|
|
642
648
|
);
|
|
643
649
|
}
|
|
644
650
|
|
|
651
|
+
function prefersReducedMotion() {
|
|
652
|
+
try {
|
|
653
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
654
|
+
} catch (error) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Replay a one-shot CSS animation class, even if it is already on. */
|
|
660
|
+
function pulseClass(element, className, duration) {
|
|
661
|
+
if (!element) return;
|
|
662
|
+
element.classList.remove(className);
|
|
663
|
+
// force a style flush so a repeated pulse restarts instead of being ignored
|
|
664
|
+
void element.offsetWidth;
|
|
665
|
+
element.classList.add(className);
|
|
666
|
+
window.setTimeout(() => element.classList.remove(className), duration);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** The carried row leans towards the anchor: dim and shrink the ghost. */
|
|
670
|
+
function setGhostArmed(gesture, armed) {
|
|
671
|
+
const ghost = gesture.ghost;
|
|
672
|
+
if (!ghost || !ghost.isConnected) return;
|
|
673
|
+
if (ghost.classList.contains("dso-ghost-armed") === armed) return;
|
|
674
|
+
ghost.classList.toggle("dso-ghost-armed", armed);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Fly the carried ghost into the "More" anchor, then tuck the row away.
|
|
679
|
+
* Only transform/opacity animate, and only one flight runs at a time; a drop
|
|
680
|
+
* that arrives mid-flight lands immediately so ghosts can never pile up.
|
|
681
|
+
*/
|
|
682
|
+
function absorbIntoMore(gesture) {
|
|
683
|
+
const moreButton =
|
|
684
|
+
gesture.moreButton && gesture.moreButton.isConnected ? gesture.moreButton : null;
|
|
685
|
+
const ghost = gesture.ghost;
|
|
686
|
+
if (!ghost || !ghost.isConnected || !moreButton || prefersReducedMotion() || state.absorbing) {
|
|
687
|
+
if (moreButton) pulseClass(moreButton, "dso-drop-receive", 260);
|
|
688
|
+
if (ghost && ghost.parentNode) ghost.remove();
|
|
689
|
+
if (alive()) hideKey(gesture.key);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const ghostRect = ghost.getBoundingClientRect();
|
|
694
|
+
const moreRect = moreButton.getBoundingClientRect();
|
|
695
|
+
const dx = moreRect.left + moreRect.width / 2 - (ghostRect.left + ghostRect.width / 2);
|
|
696
|
+
const dy = moreRect.top + moreRect.height / 2 - (ghostRect.top + ghostRect.height / 2);
|
|
697
|
+
|
|
698
|
+
let animation = null;
|
|
699
|
+
try {
|
|
700
|
+
animation = ghost.animate(
|
|
701
|
+
[
|
|
702
|
+
{ transform: "translate(0px, 0px) scale(1)", opacity: 0.96, offset: 0 },
|
|
703
|
+
{
|
|
704
|
+
transform: `translate(${dx * 0.55}px, ${dy * 0.55}px) scale(0.7)`,
|
|
705
|
+
opacity: 0.9,
|
|
706
|
+
offset: 0.55,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
transform: `translate(${dx}px, ${dy}px) scale(0.12)`,
|
|
710
|
+
opacity: 0,
|
|
711
|
+
offset: 1,
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
{ duration: 180, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" }
|
|
715
|
+
);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
animation = null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
pulseClass(moreButton, "dso-drop-receive", 260);
|
|
721
|
+
|
|
722
|
+
if (!animation) {
|
|
723
|
+
if (ghost.parentNode) ghost.remove();
|
|
724
|
+
if (alive()) hideKey(gesture.key);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
state.absorbing = animation;
|
|
729
|
+
const finish = () => {
|
|
730
|
+
if (state.absorbing === animation) state.absorbing = null;
|
|
731
|
+
if (ghost.parentNode) ghost.remove();
|
|
732
|
+
if (alive()) hideKey(gesture.key);
|
|
733
|
+
};
|
|
734
|
+
if (animation.finished && typeof animation.finished.then === "function") {
|
|
735
|
+
animation.finished.then(finish, finish);
|
|
736
|
+
} else {
|
|
737
|
+
window.setTimeout(finish, 200);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
645
741
|
function updateTarget(gesture) {
|
|
646
742
|
const indicator = gesture.indicator;
|
|
647
743
|
if (!indicator) return;
|
|
@@ -703,6 +799,7 @@ function createRuntime(ctx) {
|
|
|
703
799
|
gesture.moreButton = snapshot.moreButton;
|
|
704
800
|
if (gesture.moreButton) gesture.moreButton.classList.add("dso-drop-more");
|
|
705
801
|
}
|
|
802
|
+
setGhostArmed(gesture, true);
|
|
706
803
|
if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
|
|
707
804
|
gesture.target = { kind: "hide" };
|
|
708
805
|
return;
|
|
@@ -712,6 +809,7 @@ function createRuntime(ctx) {
|
|
|
712
809
|
gesture.moreButton.classList.remove("dso-drop-more");
|
|
713
810
|
gesture.moreButton = null;
|
|
714
811
|
}
|
|
812
|
+
setGhostArmed(gesture, false);
|
|
715
813
|
|
|
716
814
|
if (!insideRect(snapshot.navRect, x, y, 24) || others.length === 0) {
|
|
717
815
|
if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
|
|
@@ -760,7 +858,15 @@ function createRuntime(ctx) {
|
|
|
760
858
|
const { key, target, source } = gesture;
|
|
761
859
|
if (!target) return;
|
|
762
860
|
if (target.kind === "hide") {
|
|
763
|
-
hideKey
|
|
861
|
+
// Last-visible-row protection lives in hideKey; bounce the anchor instead
|
|
862
|
+
// of starting an absorb flight that would end in nothing.
|
|
863
|
+
const { rows } = visibleRows();
|
|
864
|
+
const visible = visibleKeys(rows);
|
|
865
|
+
if (!visible.includes(key) || visible.length <= 1) {
|
|
866
|
+
pulseClass(gesture.moreButton, "dso-drop-reject", 240);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
absorbIntoMore(gesture);
|
|
764
870
|
return;
|
|
765
871
|
}
|
|
766
872
|
if (target.kind === "place-menu") {
|
|
@@ -820,7 +926,10 @@ function createRuntime(ctx) {
|
|
|
820
926
|
if (!gesture) return;
|
|
821
927
|
if (gesture.phase === "dragging") {
|
|
822
928
|
const pending = gesture;
|
|
823
|
-
|
|
929
|
+
// A drop on the anchor keeps the ghost for the absorb flight; every other
|
|
930
|
+
// drop tears the drag down first so commits never see stale classes.
|
|
931
|
+
const absorbing = Boolean(pending.target && pending.target.kind === "hide");
|
|
932
|
+
clearGesture({ keepGhost: absorbing });
|
|
824
933
|
commitDrag(pending);
|
|
825
934
|
state.guardUntil = Date.now() + CLICK_GUARD_MS;
|
|
826
935
|
return;
|
|
@@ -959,6 +1068,21 @@ function createRuntime(ctx) {
|
|
|
959
1068
|
function stop() {
|
|
960
1069
|
state.stopped = true;
|
|
961
1070
|
clearGesture();
|
|
1071
|
+
if (state.absorbing) {
|
|
1072
|
+
try {
|
|
1073
|
+
state.absorbing.cancel();
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
/* ignore */
|
|
1076
|
+
}
|
|
1077
|
+
state.absorbing = null;
|
|
1078
|
+
}
|
|
1079
|
+
// the absorb flight may have left a ghost or a one-shot class behind
|
|
1080
|
+
for (const ghost of document.querySelectorAll(".dso-ghost")) ghost.remove();
|
|
1081
|
+
for (const node of document.querySelectorAll(
|
|
1082
|
+
".dso-drop-more, .dso-drop-receive, .dso-drop-reject"
|
|
1083
|
+
)) {
|
|
1084
|
+
node.classList.remove("dso-drop-more", "dso-drop-receive", "dso-drop-reject");
|
|
1085
|
+
}
|
|
962
1086
|
if (state.pollTimer) {
|
|
963
1087
|
window.clearInterval(state.pollTimer);
|
|
964
1088
|
state.pollTimer = 0;
|
package/src/styles.js
CHANGED
|
@@ -31,10 +31,51 @@ const CSS = `
|
|
|
31
31
|
button[data-dso-key].dso-dragging { opacity: 0.35; }
|
|
32
32
|
button[data-dso-key].dso-pressing { transform: scale(0.94); transition: transform 120ms ease; }
|
|
33
33
|
button[data-dso-key] { -webkit-user-select: none; user-select: none; }
|
|
34
|
-
|
|
34
|
+
/* The "More" anchor, armed: it breathes a ring and dips its glyph to read as
|
|
35
|
+
"release here and this row drops into me". */
|
|
36
|
+
button.dso-drop-more {
|
|
35
37
|
outline: 2px dashed var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
36
38
|
outline-offset: -2px;
|
|
37
39
|
background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08));
|
|
40
|
+
animation: dso-more-breathe 1100ms ease-in-out infinite;
|
|
41
|
+
}
|
|
42
|
+
button.dso-drop-more span[aria-hidden="true"] {
|
|
43
|
+
transform: translateY(2px) scale(0.88);
|
|
44
|
+
transition: transform 160ms ease;
|
|
45
|
+
}
|
|
46
|
+
@keyframes dso-more-breathe {
|
|
47
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(77, 107, 254, 0.38); }
|
|
48
|
+
55% { box-shadow: 0 0 0 8px rgba(77, 107, 254, 0); }
|
|
49
|
+
100% { box-shadow: 0 0 0 0 rgba(77, 107, 254, 0); }
|
|
50
|
+
}
|
|
51
|
+
@keyframes dso-more-receive {
|
|
52
|
+
0% { transform: scale(1); }
|
|
53
|
+
35% { transform: scale(0.86); }
|
|
54
|
+
70% { transform: scale(1.06); }
|
|
55
|
+
100% { transform: scale(1); }
|
|
56
|
+
}
|
|
57
|
+
button.dso-drop-receive { animation: dso-more-receive 260ms cubic-bezier(0.2, 0.7, 0.3, 1); }
|
|
58
|
+
@keyframes dso-drop-reject {
|
|
59
|
+
0%, 100% { transform: translateX(0); }
|
|
60
|
+
20% { transform: translateX(-4px); }
|
|
61
|
+
40% { transform: translateX(4px); }
|
|
62
|
+
60% { transform: translateX(-3px); }
|
|
63
|
+
80% { transform: translateX(2px); }
|
|
64
|
+
}
|
|
65
|
+
button.dso-drop-reject { animation: dso-drop-reject 240ms ease; }
|
|
66
|
+
/* Carrying a row towards the anchor: the ghost dims and shrinks a little, so
|
|
67
|
+
its eventual absorption does not feel like a jump. */
|
|
68
|
+
.dso-ghost.dso-ghost-armed {
|
|
69
|
+
opacity: 0.82;
|
|
70
|
+
transform: scale(0.94);
|
|
71
|
+
transition: transform 140ms ease, opacity 140ms ease;
|
|
72
|
+
}
|
|
73
|
+
@media (prefers-reduced-motion: reduce) {
|
|
74
|
+
button.dso-drop-more,
|
|
75
|
+
button.dso-drop-receive,
|
|
76
|
+
button.dso-drop-reject { animation: none !important; }
|
|
77
|
+
.dso-ghost,
|
|
78
|
+
.dso-ghost.dso-ghost-armed { transition: none !important; }
|
|
38
79
|
}
|
|
39
80
|
body.dso-dragging,
|
|
40
81
|
body.dso-dragging * { cursor: grabbing !important; }
|