dsh-sidebar-organizer 0.4.0 → 0.4.1

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 CHANGED
@@ -3,6 +3,24 @@
3
3
  版本格式遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
4
4
  本文件的中文条目为默认语言;英文条目见英文段落(同一版本内)。
5
5
 
6
+ ## 0.4.1 — 2026-11-02
7
+
8
+ - 修复反复拖动时排序“抽搐”:落点改为按当前视觉顺序(`layout.order` +
9
+ 内联 `style.order`)计算,并把目标位置锚定到具体某一行的 key,而不是页面里
10
+ 的 DOM 序号;长按成立时会先强制落一次布局,避免用上一帧的旧几何做判定。
11
+ - 「更多」的投放判定收窄:只有指针真正落在「更多」按钮矩形内、且位于最后一
12
+ 行下方时才收纳,拖到列表末尾不再被误判成收纳。
13
+
14
+ ### English
15
+
16
+ - Fixed jittery ordering after repeated drags: the drop index is now computed
17
+ from the visual order (`layout.order` plus inline `style.order`) and anchored
18
+ to a row key instead of the DOM index, and a layout pass is forced when the
19
+ long-press succeeds so the frozen geometry is current.
20
+ - Tightened the “More” drop zone: a row is tucked away only when the pointer is
21
+ inside the anchor's own rectangle and below the last row, so dropping at the
22
+ end of the list no longer hides it by mistake.
23
+
6
24
  ## 0.4.0 — 2026-11-02
7
25
 
8
26
  - 源码拆分为 `src/` 下的模块(constants / styles / storage / icons / slot /
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.0**: sources split into `src/` modules with a build script;
10
- > AI-usage and code-origin disclosure added; multilingual docs.
9
+ > Version **0.4.1**: fixes jittery ordering after repeated drags and tightens
10
+ > the “More” drop zone.
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.0**:源码拆分为 `src/` 模块并加入构建脚本;补齐 AI 使用与代码来源声明;多语言文档。
7
+ > 当前版本 **0.4.1**:修复反复拖动时的排序抖动,并收窄「更多」的投放判定。
8
8
 
9
9
  ## 现在能做什么
10
10
 
@@ -132,7 +132,8 @@ 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
+ - [x] 0.4.1 — 修复反复拖动时的排序抖动与「更多」误收纳
136
137
  - [ ] 0.5.0 — 无障碍补强(菜单内上移 / 下移按钮、屏幕阅读器朗读顺序变化)
137
138
 
138
139
  ## 声明与引用
package/lib/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dsh-sidebar-organizer — client half (v0.4.0).
2
+ * dsh-sidebar-organizer — client half (v0.4.1).
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.0";
18
+ const VERSION = "0.4.1";
19
19
 
20
20
  const PANEL_SLOT = "sidebar.panellist";
21
21
  const ACTIONS_SLOT = "sidebar.panellist.actions";
@@ -424,8 +424,24 @@ function createRuntime(ctx) {
424
424
  return { nav, rows, moreHost, moreButton };
425
425
  }
426
426
 
427
+ /**
428
+ * Keys of the rows currently visible in the strip, in the order the user sees
429
+ * them — i.e. the persisted layout order, NOT the DOM order. The sidebar keeps
430
+ * rendering rows in registration order and we only move them with CSS
431
+ * `order`, so a DOM-order list would shift every time a drag committed and
432
+ * make drops land somewhere other than where the user released the pointer.
433
+ */
427
434
  function visibleKeys(rows) {
428
- return rows.filter((row) => !row.hidden).map((row) => row.key);
435
+ const layout = readLayout();
436
+ const visible = rows.filter((row) => !row.hidden).map((row) => row.key);
437
+ const ordered = [];
438
+ for (const key of layout.order) {
439
+ if (visible.includes(key) && !ordered.includes(key)) ordered.push(key);
440
+ }
441
+ for (const key of visible) {
442
+ if (!ordered.includes(key)) ordered.push(key);
443
+ }
444
+ return ordered;
429
445
  }
430
446
 
431
447
  function applyOrder(nav, rows, moreHost, moreButton) {
@@ -500,11 +516,13 @@ function createRuntime(ctx) {
500
516
  }
501
517
  }
502
518
 
503
- function pass() {
519
+ function pass(force) {
504
520
  if (!alive()) return;
505
521
  // A pass would fight the drag ghost for layout: rows must not move while a
506
- // row is being carried. Re-run once the gesture is over.
507
- if (state.gesture) {
522
+ // row is being carried. Re-run once the gesture is over. `force` is used
523
+ // right before a drag starts to make sure the DOM already reflects the
524
+ // persisted order, so frozen rects and the committed order agree.
525
+ if (state.gesture && !force) {
508
526
  state.passDeferred = true;
509
527
  return;
510
528
  }
@@ -778,6 +796,16 @@ function createRuntime(ctx) {
778
796
  clearGesture();
779
797
  return;
780
798
  }
799
+ // Flush any pending layout before freezing rects: after a previous drop the
800
+ // rAF pass may not have run yet, and stale visuals would make the next drop
801
+ // land somewhere else than where the pointer is.
802
+ try {
803
+ pass(true);
804
+ } catch (error) {
805
+ /* a failed flush must not block the drag */
806
+ }
807
+ if (!element.isConnected) element = locateRowButton(gesture.key) || element;
808
+ gesture.element = element;
781
809
  const rect = element.getBoundingClientRect();
782
810
  if (rect.width === 0 || rect.height === 0) {
783
811
  clearGesture();
@@ -955,16 +983,25 @@ function createRuntime(ctx) {
955
983
  gesture.target = null;
956
984
  }
957
985
 
958
- // Dropping a sidebar row on the "More" anchor tucks it away.
959
- if (gesture.source === "nav" && snapshot.moreRect && insideRect(snapshot.moreRect, x, y, 6)) {
960
- if (gesture.moreButton !== snapshot.moreButton) {
961
- if (gesture.moreButton) gesture.moreButton.classList.remove("dso-drop-more");
962
- gesture.moreButton = snapshot.moreButton;
963
- if (gesture.moreButton) gesture.moreButton.classList.add("dso-drop-more");
986
+ // Dropping a sidebar row on the "More" anchor tucks it away. The anchor is
987
+ // a small target: require the pointer to be inside its own rectangle
988
+ // (no padding) and strictly below the last row, otherwise a drop meant for
989
+ // "end of the strip" would quietly hide the row instead of reordering it.
990
+ if (gesture.source === "nav" && snapshot.moreRect) {
991
+ const lastBottom = others.length
992
+ ? others[others.length - 1].rect.top + others[others.length - 1].rect.height
993
+ : -Infinity;
994
+ const overMore = insideRect(snapshot.moreRect, x, y, 0) && y > lastBottom + 1;
995
+ if (overMore) {
996
+ if (gesture.moreButton !== snapshot.moreButton) {
997
+ if (gesture.moreButton) gesture.moreButton.classList.remove("dso-drop-more");
998
+ gesture.moreButton = snapshot.moreButton;
999
+ if (gesture.moreButton) gesture.moreButton.classList.add("dso-drop-more");
1000
+ }
1001
+ if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
1002
+ gesture.target = { kind: "hide" };
1003
+ return;
964
1004
  }
965
- if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
966
- gesture.target = { kind: "hide" };
967
- return;
968
1005
  }
969
1006
  if (gesture.moreButton) {
970
1007
  gesture.moreButton.classList.remove("dso-drop-more");
@@ -985,7 +1022,15 @@ function createRuntime(ctx) {
985
1022
  break;
986
1023
  }
987
1024
  }
988
- gesture.target = { kind: "place", index };
1025
+ // Remember which row the drop line sits in front of. Indices are computed
1026
+ // from frozen rects (the visual order at drag start) while commits apply to
1027
+ // the persisted order, so carrying the neighbour key keeps the two aligned
1028
+ // even when another drop has just rewritten the layout.
1029
+ gesture.target = {
1030
+ kind: "place",
1031
+ index,
1032
+ beforeKey: index < others.length ? others[index].key : null,
1033
+ };
989
1034
 
990
1035
  if (index === snapshot.lastIndex) {
991
1036
  if (indicator.dataset.visible !== "1") indicator.dataset.visible = "1";
@@ -1018,8 +1063,16 @@ function createRuntime(ctx) {
1018
1063
  return;
1019
1064
  }
1020
1065
  if (target.kind === "place") {
1066
+ // Map the drop position onto the persisted order (see visibleKeys): the
1067
+ // anchor row is the key we resolved from the frozen rects.
1068
+ const { rows } = visibleRows();
1069
+ const others = visibleKeys(rows).filter((item) => item !== key);
1070
+ let index = Math.max(0, Math.min(target.index, others.length));
1071
+ if (target.beforeKey && others.includes(target.beforeKey)) {
1072
+ index = others.indexOf(target.beforeKey);
1073
+ }
1021
1074
  if (source === "menu") {
1022
- revealKey(key, target.index);
1075
+ revealKey(key, index);
1023
1076
  if (state.dismissMenu) {
1024
1077
  try {
1025
1078
  state.dismissMenu();
@@ -1028,7 +1081,7 @@ function createRuntime(ctx) {
1028
1081
  }
1029
1082
  }
1030
1083
  } else {
1031
- reorderKey(key, target.index);
1084
+ reorderKey(key, index);
1032
1085
  }
1033
1086
  }
1034
1087
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidebar-organizer",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "DSH 侧栏面板整理器:长按拖动排序、拖入/拖出「更多」收纳、键盘排序",
6
6
  "type": "module",
package/src/runtime.js CHANGED
@@ -129,8 +129,24 @@ function createRuntime(ctx) {
129
129
  return { nav, rows, moreHost, moreButton };
130
130
  }
131
131
 
132
+ /**
133
+ * Keys of the rows currently visible in the strip, in the order the user sees
134
+ * them — i.e. the persisted layout order, NOT the DOM order. The sidebar keeps
135
+ * rendering rows in registration order and we only move them with CSS
136
+ * `order`, so a DOM-order list would shift every time a drag committed and
137
+ * make drops land somewhere other than where the user released the pointer.
138
+ */
132
139
  function visibleKeys(rows) {
133
- return rows.filter((row) => !row.hidden).map((row) => row.key);
140
+ const layout = readLayout();
141
+ const visible = rows.filter((row) => !row.hidden).map((row) => row.key);
142
+ const ordered = [];
143
+ for (const key of layout.order) {
144
+ if (visible.includes(key) && !ordered.includes(key)) ordered.push(key);
145
+ }
146
+ for (const key of visible) {
147
+ if (!ordered.includes(key)) ordered.push(key);
148
+ }
149
+ return ordered;
134
150
  }
135
151
 
136
152
  function applyOrder(nav, rows, moreHost, moreButton) {
@@ -205,11 +221,13 @@ function createRuntime(ctx) {
205
221
  }
206
222
  }
207
223
 
208
- function pass() {
224
+ function pass(force) {
209
225
  if (!alive()) return;
210
226
  // A pass would fight the drag ghost for layout: rows must not move while a
211
- // row is being carried. Re-run once the gesture is over.
212
- if (state.gesture) {
227
+ // row is being carried. Re-run once the gesture is over. `force` is used
228
+ // right before a drag starts to make sure the DOM already reflects the
229
+ // persisted order, so frozen rects and the committed order agree.
230
+ if (state.gesture && !force) {
213
231
  state.passDeferred = true;
214
232
  return;
215
233
  }
@@ -483,6 +501,16 @@ function createRuntime(ctx) {
483
501
  clearGesture();
484
502
  return;
485
503
  }
504
+ // Flush any pending layout before freezing rects: after a previous drop the
505
+ // rAF pass may not have run yet, and stale visuals would make the next drop
506
+ // land somewhere else than where the pointer is.
507
+ try {
508
+ pass(true);
509
+ } catch (error) {
510
+ /* a failed flush must not block the drag */
511
+ }
512
+ if (!element.isConnected) element = locateRowButton(gesture.key) || element;
513
+ gesture.element = element;
486
514
  const rect = element.getBoundingClientRect();
487
515
  if (rect.width === 0 || rect.height === 0) {
488
516
  clearGesture();
@@ -660,16 +688,25 @@ function createRuntime(ctx) {
660
688
  gesture.target = null;
661
689
  }
662
690
 
663
- // Dropping a sidebar row on the "More" anchor tucks it away.
664
- if (gesture.source === "nav" && snapshot.moreRect && insideRect(snapshot.moreRect, x, y, 6)) {
665
- if (gesture.moreButton !== snapshot.moreButton) {
666
- if (gesture.moreButton) gesture.moreButton.classList.remove("dso-drop-more");
667
- gesture.moreButton = snapshot.moreButton;
668
- if (gesture.moreButton) gesture.moreButton.classList.add("dso-drop-more");
691
+ // Dropping a sidebar row on the "More" anchor tucks it away. The anchor is
692
+ // a small target: require the pointer to be inside its own rectangle
693
+ // (no padding) and strictly below the last row, otherwise a drop meant for
694
+ // "end of the strip" would quietly hide the row instead of reordering it.
695
+ if (gesture.source === "nav" && snapshot.moreRect) {
696
+ const lastBottom = others.length
697
+ ? others[others.length - 1].rect.top + others[others.length - 1].rect.height
698
+ : -Infinity;
699
+ const overMore = insideRect(snapshot.moreRect, x, y, 0) && y > lastBottom + 1;
700
+ if (overMore) {
701
+ if (gesture.moreButton !== snapshot.moreButton) {
702
+ if (gesture.moreButton) gesture.moreButton.classList.remove("dso-drop-more");
703
+ gesture.moreButton = snapshot.moreButton;
704
+ if (gesture.moreButton) gesture.moreButton.classList.add("dso-drop-more");
705
+ }
706
+ if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
707
+ gesture.target = { kind: "hide" };
708
+ return;
669
709
  }
670
- if (indicator.dataset.visible !== "0") indicator.dataset.visible = "0";
671
- gesture.target = { kind: "hide" };
672
- return;
673
710
  }
674
711
  if (gesture.moreButton) {
675
712
  gesture.moreButton.classList.remove("dso-drop-more");
@@ -690,7 +727,15 @@ function createRuntime(ctx) {
690
727
  break;
691
728
  }
692
729
  }
693
- gesture.target = { kind: "place", index };
730
+ // Remember which row the drop line sits in front of. Indices are computed
731
+ // from frozen rects (the visual order at drag start) while commits apply to
732
+ // the persisted order, so carrying the neighbour key keeps the two aligned
733
+ // even when another drop has just rewritten the layout.
734
+ gesture.target = {
735
+ kind: "place",
736
+ index,
737
+ beforeKey: index < others.length ? others[index].key : null,
738
+ };
694
739
 
695
740
  if (index === snapshot.lastIndex) {
696
741
  if (indicator.dataset.visible !== "1") indicator.dataset.visible = "1";
@@ -723,8 +768,16 @@ function createRuntime(ctx) {
723
768
  return;
724
769
  }
725
770
  if (target.kind === "place") {
771
+ // Map the drop position onto the persisted order (see visibleKeys): the
772
+ // anchor row is the key we resolved from the frozen rects.
773
+ const { rows } = visibleRows();
774
+ const others = visibleKeys(rows).filter((item) => item !== key);
775
+ let index = Math.max(0, Math.min(target.index, others.length));
776
+ if (target.beforeKey && others.includes(target.beforeKey)) {
777
+ index = others.indexOf(target.beforeKey);
778
+ }
726
779
  if (source === "menu") {
727
- revealKey(key, target.index);
780
+ revealKey(key, index);
728
781
  if (state.dismissMenu) {
729
782
  try {
730
783
  state.dismissMenu();
@@ -733,7 +786,7 @@ function createRuntime(ctx) {
733
786
  }
734
787
  }
735
788
  } else {
736
- reorderKey(key, target.index);
789
+ reorderKey(key, index);
737
790
  }
738
791
  }
739
792
  }