dsh-fonttune 0.7.0 → 0.7.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.en.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.7.1] - 2026-10-20
6
+
7
+ ### Changed
8
+ - **The panel and the dot each decide their own side**: the panel opens into the quadrant its own middle sits in, while the dot still snaps to the corner it remembers, so the two no longer share one rule.
9
+ - **Switching corners takes less travel**: the conversation's middle is the line to cross, so the corner follows the panel's own middle rather than the region's edge.
10
+ - **Snapping is one continuous motion**: after a release the panel and the dot travel to the target corner together, instead of the panel jumping to the other side first and then sliding.
11
+
12
+ ### Fixed
13
+ - **The panel sometimes rested on the window's bottom-left corner**: while the conversation was not measurable yet, the dot was moved onto the window corner and that position was stored; it now moves only against a measurable conversation and never stores a stand-in.
14
+ - **The dot sat on the sidebar in a narrow window**: a conversation squeezed to a sliver is still the region, instead of falling back to the whole window.
15
+ - **The shadow did not follow the unfold**: the panel's shadow now changes with the shape while it unfolds and folds.
16
+ - **The first frame of an unfold flickered**: the panel no longer blinks before it appears.
17
+
5
18
  ## [0.7.0] - 2026-10-19
6
19
 
7
20
  ### Changed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  **dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.7.1] - 2026-10-20
6
+
7
+ ### 变更
8
+ - **面板与圆点各自决定吸附方向**:面板按自己的中心落在对话区的哪个象限展开,圆点仍吸附到自己记住的角落,两者不再共用一套判断。
9
+ - **换角所需移动更少**:以对话区中心为原点,面板中心越过中线即可换到对应的角,不必先拖到区域边缘。
10
+ - **吸附是一个连续动作**:换角松手后,面板与圆点一同滑向目标角落,面板不再先跳到另一侧再飞过去。
11
+
12
+ ### 修复
13
+ - **悬浮面板偶尔停在窗口左下角**:对话区尚未完成布局的瞬间,圆点会被移到窗口角落并记住那个位置;现在只在对话区可测量时移动,也不再保存这种临时位置。
14
+ - **窗口很窄时圆点落在侧边栏上**:对话区被挤成一条窄边时仍按对话区吸附,不再退回整块窗口。
15
+ - **展开与收起时投影不同步**:面板的投影现在随展开、收起的形状一同变化。
16
+ - **展开的第一帧闪动**:面板展开时不再先闪一下再出现。
17
+
5
18
  ## [0.7.0] - 2026-10-19
6
19
 
7
20
  ### 变更
package/README.en.md CHANGED
@@ -39,7 +39,7 @@ The floating tuning panel (left, with its "in force right now" readout) and the
39
39
 
40
40
  | Plugin version | Supported DSH versions |
41
41
  | --- | --- |
42
- | **0.7.x** (latest, 0.7.0) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2 |
42
+ | **0.7.x** (latest, 0.7.1) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2 |
43
43
  | 0.6.x (0.6.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2, 0.2.1-alpha.3, 0.2.1-alpha.4, 0.2.1-alpha.5, 0.2.1-rc.1, 0.2.1-rc.2 |
44
44
  | 0.5.x (0.5.0) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
45
45
  | 0.4.x (0.4.2) | 0.1.7-alpha.1, 0.1.7-alpha.2, 0.1.7-rc.1, 0.1.7-rc.2, 0.2.0-rc.1, 0.2.0-rc.2, 0.2.1-alpha.1, 0.2.1-alpha.2 |
package/README.md CHANGED
@@ -39,7 +39,7 @@
39
39
 
40
40
  | 插件版本 | 支持的 DSH 版本 |
41
41
  | --- | --- |
42
- | **0.7.x**(最新,0.7.0) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2 |
42
+ | **0.7.x**(最新,0.7.1) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2 |
43
43
  | 0.6.x(0.6.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.1、0.2.1-rc.2 |
44
44
  | 0.5.x(0.5.0) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
45
45
  | 0.4.x(0.4.2) | 0.1.7-alpha.1、0.1.7-alpha.2、0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.2 |
package/lib/client.js CHANGED
@@ -1194,6 +1194,15 @@ function floatBoundsPick(rects, viewport) {
1194
1194
  if (!isFinite(vh) || vh <= 0) vh = FLOAT_VIEWPORT_H;
1195
1195
  var full = { left: 0, top: 0, right: vw, bottom: vh, width: vw, height: vh };
1196
1196
  var list = Array.isArray(rects) ? rects : [];
1197
+ // A region narrower than one dot used to count as "no region at all", and the
1198
+ // fallback was the WHOLE viewport — so in a narrow window the dot parked on the
1199
+ // window's own corner, over the sidebar, instead of on the conversation's. The
1200
+ // largest readable box is kept as a fallback, and it names itself in `index` so
1201
+ // the watcher holds the box whose size actually defines the region. `full` is
1202
+ // left for a page with no region at all — or only degenerate 0-area rects.
1203
+ var best = null;
1204
+ var bestIndex = -1;
1205
+ var bestArea = -1;
1197
1206
  for (var index = 0; index < list.length; index += 1) {
1198
1207
  var box = floatBoxOf(list[index]);
1199
1208
  if (box === null) continue;
@@ -1203,12 +1212,18 @@ function floatBoundsPick(rects, viewport) {
1203
1212
  var bottom = Math.max(0, Math.min(vh, box.bottom));
1204
1213
  var width = right - left;
1205
1214
  var height = bottom - top;
1215
+ if (width > 0 && height > 0 && width * height > bestArea) {
1216
+ bestArea = width * height;
1217
+ bestIndex = index;
1218
+ best = { left: left, top: top, right: right, bottom: bottom, width: width, height: height };
1219
+ }
1206
1220
  if (width < FLOAT_BOUNDS_MIN || height < FLOAT_BOUNDS_MIN) continue;
1207
1221
  return {
1208
1222
  index: index,
1209
1223
  bounds: { left: left, top: top, right: right, bottom: bottom, width: width, height: height },
1210
1224
  };
1211
1225
  }
1226
+ if (best !== null) return { index: bestIndex, bounds: best };
1212
1227
  return { index: -1, bounds: full };
1213
1228
  }
1214
1229
 
@@ -1327,6 +1342,36 @@ function floatTransformOrigin(quadrant) {
1327
1342
  return "100% 100%";
1328
1343
  }
1329
1344
 
1345
+ /**
1346
+ * Which side the open CARD grows towards: the one with more room for it.
1347
+ *
1348
+ * The card hangs off the dot, so it can only extend one way from it, and the
1349
+ * side that holds more of the card is the side the conversation has room for.
1350
+ * Each axis compares the room beyond the dot's own box on either side and sends
1351
+ * the card to the larger one — which is also the side its middle ends up on, so
1352
+ * "the panel goes to the corner it sits in" holds by construction.
1353
+ *
1354
+ * This is the CARD's rule, not the dot's: the dot keeps the corner it remembers
1355
+ * living in (see `floatCornerPoint`), while a rule shared by both is what made
1356
+ * the card jump whenever the dot crossed a line the card had not. The answer
1357
+ * depends on the dot and the region and on nothing else — never on a card it
1358
+ * just placed — so it cannot vote itself back and forth.
1359
+ * @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
1360
+ * @param {number} x - the dot's left edge, in CSS pixels.
1361
+ * @param {number} y - the dot's top edge, in CSS pixels.
1362
+ * @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
1363
+ * @returns {string} one of "tl", "tr", "bl", "br" — the quadrant the card is
1364
+ * anchored on, which is the corner it grows away from.
1365
+ */
1366
+ function floatCardCorner(bounds, x, y, dot) {
1367
+ var box = floatBoxOf(bounds);
1368
+ if (box === null) box = { left: 0, top: 0, right: FLOAT_VIEWPORT_W, bottom: FLOAT_VIEWPORT_H };
1369
+ var size = dot === undefined ? FLOAT_DOT : dot;
1370
+ var growRight = box.right - x >= x + size - box.left;
1371
+ var growDown = box.bottom - y >= y + size - box.top;
1372
+ return (growDown ? "t" : "b") + (growRight ? "l" : "r");
1373
+ }
1374
+
1330
1375
  /**
1331
1376
  * Which corner of the card lands on the dot, and where the expansion starts.
1332
1377
  *
@@ -4623,6 +4668,7 @@ var shared = {
4623
4668
  floatExpandDirection: floatExpandDirection,
4624
4669
  floatTransformOrigin: floatTransformOrigin,
4625
4670
  floatCardAnchor: floatCardAnchor,
4671
+ floatCardCorner: floatCardCorner,
4626
4672
  floatClipStart: floatClipStart,
4627
4673
  floatClipRest: floatClipRest,
4628
4674
  isRestClip: isRestClip,
@@ -10377,7 +10423,10 @@ var GLASS_OK = (function () {
10377
10423
 
10378
10424
  function mountFloatPanel(scope, t, hostCtx) {
10379
10425
  /** How far the panel's drop shadow reaches past its own box (10px offset + blur). */
10380
- var SHADOW_REACH = 56; var noop = function () {};
10426
+ var SHADOW_REACH = 56;
10427
+ /** The dot's own snap slide, in ms — the stylesheet's `.dfp-floatSnap` transition. */
10428
+ var FLOAT_SNAP_MS = 160;
10429
+ var noop = function () {};
10381
10430
  if (typeof document === "undefined" || !document.body) return { destroy: noop, toggle: noop };
10382
10431
  // The offline stand-ins hand out element stubs without listeners or
10383
10432
  // selectors; without a real element face there is nothing to mount on.
@@ -10468,6 +10517,19 @@ function mountFloatPanel(scope, t, hostCtx) {
10468
10517
  // end on the same small rectangle the unfold started from, and the card's own
10469
10518
  // measurement only exists while it is mounted.
10470
10519
  var liveQuadrant = null;
10520
+ // The CARD's own side, re-decided when the panel opens (and when the dot is
10521
+ // dropped or the region changes shape). It is NOT the dot's corner: the card
10522
+ // goes wherever it has the most room, while the dot keeps the corner it
10523
+ // remembers living in. One rule shared by both is what made the card jump
10524
+ // whenever the dot crossed a line the card had not.
10525
+ var cardQuadrant = null;
10526
+ // Whether the NEXT render follows a snap. The card then keeps the place it
10527
+ // already had on screen and TRAVELS to the new corner with the dot, instead of
10528
+ // jumping a panel-width to the other side of it (see the glide in `render`).
10529
+ var anchorGlide = false;
10530
+ // The card's own offset inside the host, as laid out last time: the glide is
10531
+ // the difference between where the new side puts it and that.
10532
+ var lastAnchor = null;
10471
10533
 
10472
10534
  var el = function (tag, className, text) {
10473
10535
  var node = document.createElement(tag);
@@ -10846,6 +10908,11 @@ function mountFloatPanel(scope, t, hostCtx) {
10846
10908
  return null;
10847
10909
  };
10848
10910
 
10911
+ // Whether the last region reading was a real region or the whole-window
10912
+ // stand-in (see `floatBounds`). A stand-in never moves the dot: it means "no
10913
+ // conversation to measure right now", not "the conversation is the window".
10914
+ var boundsAreReal = false;
10915
+
10849
10916
  /** The window size the bounds are cut to, with the shared fallback. */
10850
10917
  var floatViewport = function () {
10851
10918
  return {
@@ -10975,10 +11042,19 @@ function mountFloatPanel(scope, t, hostCtx) {
10975
11042
  * The conversation when it can be found, the whole window when it cannot (a
10976
11043
  * settings page has no prose at all): the window is what this used to be
10977
11044
  * everywhere, and it stays the last resort rather than the rule.
11045
+ *
11046
+ * WHICH one answered is remembered in `boundsAreReal`, because the two are not
11047
+ * interchangeable for a MOVE: the whole window is a stand-in for "there is no
11048
+ * conversation to measure at this instant" — a session still mounting, a
11049
+ * message list laid out at 0×0 — and a dot parked on the window's corner over
11050
+ * a page that does have a conversation is exactly the reported "it sits on the
11051
+ * window's left edge instead of the conversation's corner".
10978
11052
  * @returns {object} the bounds, in viewport coordinates.
10979
11053
  */
10980
11054
  var floatBounds = function () {
10981
- return shared.floatBoundsFrom(floatBoundsRects(), floatViewport());
11055
+ var picked = shared.floatBoundsPick(floatBoundsRects(), floatViewport());
11056
+ boundsAreReal = picked.index >= 0;
11057
+ return picked.bounds;
10982
11058
  };
10983
11059
 
10984
11060
  /**
@@ -11067,6 +11143,36 @@ function mountFloatPanel(scope, t, hostCtx) {
11067
11143
  );
11068
11144
  };
11069
11145
 
11146
+ /**
11147
+ * The shade's scale for the panel's CURRENT visible box.
11148
+ *
11149
+ * Read from the card's own clip every frame, not driven by a CSS transition: the
11150
+ * small value only exists for the two arming frames, and a transition beginning
11151
+ * there is cleared before it can run (measured: the inline value was gone and
11152
+ * `transform` was `none` 138 ms into the unfold, with the shadow at full size the
11153
+ * whole way). Written per frame, the shadow is exactly as big as what the panel
11154
+ * is showing, on every frame, by construction.
11155
+ * @param {object} card - the live card.
11156
+ * @param {string} clip - its computed clip-path.
11157
+ * @returns {string} the inline transform, or "" for the resting panel.
11158
+ */
11159
+ var shadeFromClip = function (card, clip) {
11160
+ if (clip === "none") return "";
11161
+ var found = /^inset\((-?[\d.]+)px (-?[\d.]+)px (-?[\d.]+)px (-?[\d.]+)px/.exec(clip);
11162
+ if (found === null) return "";
11163
+ var width = 0;
11164
+ var height = 0;
11165
+ try {
11166
+ width = card.offsetWidth;
11167
+ height = card.offsetHeight;
11168
+ } catch (error) {
11169
+ return "";
11170
+ }
11171
+ if (!(width > 0) || !(height > 0)) return "";
11172
+ var visibleW = Math.max(1, width - Number(found[2]) - Number(found[4]));
11173
+ var visibleH = Math.max(1, height - Number(found[1]) - Number(found[3]));
11174
+ return "scale(" + (visibleW / width).toFixed(4) + ", " + (visibleH / height).toFixed(4) + ")";
11175
+ };
11070
11176
  var mirrorGlassClip = function (card, glass, ms) { if (glass === null || glass === undefined) return;
11071
11177
  var shade = host.querySelector(".dfp-floatShade");
11072
11178
  var until = Date.now() + ms;
@@ -11079,11 +11185,11 @@ function mountFloatPanel(scope, t, hostCtx) {
11079
11185
  return;
11080
11186
  }
11081
11187
  glass.style.clipPath = clip;
11082
- // The shade gets the same shape GROWN by the shadow's reach: its shadow
11083
- // paints OUTSIDE the box, so an exact clip would cut the shadow off, while no
11084
- // clip at all left a static frame around the growing panel — the "second edge"
11085
- // that ran alongside the panel's own.
11086
- if (shade !== null && shade !== undefined) shade.style.clipPath = growClip(clip, SHADOW_REACH);
11188
+ // The shade is SCALED to the panel's visible box, frame by frame (see
11189
+ // `shadeFromClip`): its shadow is painted from its own box, so a layer left at
11190
+ // the full panel size shows a full-size shadow for the whole unfold — the
11191
+ // "second edge" beside the panel's own. Scaling the layer scales the shadow.
11192
+ if (shade !== null && shade !== undefined) shade.style.transform = shadeFromClip(card, clip);
11087
11193
  if (Date.now() < until) globalThis.requestAnimationFrame(step);
11088
11194
  };
11089
11195
  if (typeof globalThis.requestAnimationFrame === "function") globalThis.requestAnimationFrame(step);
@@ -11430,6 +11536,56 @@ function mountFloatPanel(scope, t, hostCtx) {
11430
11536
  return fallback;
11431
11537
  };
11432
11538
 
11539
+ /**
11540
+ * The middle of the CARD as it is on the page, or null while it is not.
11541
+ *
11542
+ * Read from the layer the card sits in (the wrapper carries the anchored edge
11543
+ * and the size), so the unfold's own scale — which rides the card inside it —
11544
+ * cannot make a growing panel look like a moving one.
11545
+ * @returns {{x: number, y: number}|null} its middle in viewport coordinates.
11546
+ */
11547
+ var cardMiddle = function () {
11548
+ var layer = null;
11549
+ try {
11550
+ layer = host.querySelector(".dfp-floatShadow");
11551
+ } catch (error) {
11552
+ layer = null;
11553
+ }
11554
+ if (layer === null || layer === undefined) return null;
11555
+ var rect = rectOf(layer);
11556
+ if (rect === null || !(rect.width > 0) || !(rect.height > 0)) return null;
11557
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
11558
+ };
11559
+
11560
+ /**
11561
+ * Re-decide which side of the conversation the card opens towards.
11562
+ *
11563
+ * The rule is the card's OWN, and it is about ROOM: the card hangs off the
11564
+ * dot, so it can only grow one way from it, and the side that holds more of it
11565
+ * is the side the conversation has room for. Per axis the two rooms on either
11566
+ * side of the dot are compared and the card is sent to the larger one, which
11567
+ * is also the side its middle ends up on — the panel flies to the corner it
11568
+ * sits in, measured against the conversation rather than against the dot.
11569
+ *
11570
+ * The dot keeps its own rule — the corner it remembers living in — and that
11571
+ * difference is the point: a rule shared by both made the card jump whenever
11572
+ * the dot crossed a line the card had not. This answer depends on the dot and
11573
+ * the region and on nothing else, so it cannot vote itself back and forth.
11574
+ *
11575
+ * Called at the moments a side can legitimately change — an open, when the
11576
+ * card does not exist yet, a release that lands the dot on another corner, and
11577
+ * a region that changed shape — and never from `render`. A caller that changes
11578
+ * the side must also place the card WITHOUT a slide: a side is an anchored
11579
+ * edge, so the card moves by its own width when it changes.
11580
+ * @param {object} [bounds] - the region, when the caller has it already.
11581
+ */
11582
+ var voteCardQuadrant = function (bounds, at) {
11583
+ var region = bounds === undefined ? floatBounds() : bounds;
11584
+ var origin = at === undefined ? pos : at;
11585
+ if (origin === null || origin === undefined) return;
11586
+ cardQuadrant = shared.floatCardCorner(region, origin.x, origin.y);
11587
+ };
11588
+
11433
11589
  /**
11434
11590
  * Snap the dot to the corner it LIVES in, with a short slide.
11435
11591
  *
@@ -11452,8 +11608,15 @@ function mountFloatPanel(scope, t, hostCtx) {
11452
11608
  * to send the dot to the top-left corner the user never asked for.
11453
11609
  * @returns {{x: number, y: number}} the corner the dot now sits on.
11454
11610
  */
11455
- var snapToCorner = function () {
11611
+ var snapToCorner = function (instant) {
11456
11612
  var bounds = floatBounds();
11613
+ // A stand-in region is not a home: the window's corner is what the dot would
11614
+ // land on over a page whose conversation is merely not measurable yet, which
11615
+ // is the reported "it snaps to the window's left edge instead of the
11616
+ // conversation's corner". A dot the document already placed keeps the corner
11617
+ // the last REAL region gave it; the observer settles it again the moment one
11618
+ // exists (see `reflowIntoBounds`).
11619
+ if (!boundsAreReal && pos !== null) return pos;
11457
11620
  var snapped;
11458
11621
  if (pos !== null) {
11459
11622
  snapped = cornerHome(bounds, pos);
@@ -11475,7 +11638,11 @@ function mountFloatPanel(scope, t, hostCtx) {
11475
11638
  }
11476
11639
  pos = snapped;
11477
11640
  try {
11478
- host.classList.add("dfp-floatSnap");
11641
+ // An instant settle must also DROP a snap class left over from an earlier
11642
+ // move, or the very same `left`/`top` write is animated and the panel appears
11643
+ // to drift over from where it used to be.
11644
+ if (instant === true) host.classList.remove("dfp-floatSnap");
11645
+ else host.classList.add("dfp-floatSnap");
11479
11646
  } catch (error) {
11480
11647
  // A host without classes still lands on the snapped corner.
11481
11648
  }
@@ -11586,9 +11753,15 @@ function mountFloatPanel(scope, t, hostCtx) {
11586
11753
  * dot rather than the document handing it a position.
11587
11754
  * @returns {boolean} true when the dot was moved onto its corner.
11588
11755
  */
11589
- var settleOnCorner = function (regionMoved) {
11756
+ var settleOnCorner = function (regionMoved, instant) {
11590
11757
  if (drag !== null || resize !== null) return false;
11591
11758
  var bounds = floatBounds();
11759
+ // Nothing real to settle onto: a shut panel on a settings page, or a
11760
+ // conversation that is not measurable at this instant, leaves the dot where
11761
+ // it is — moving it onto the window's corner is the bug, not the fallback.
11762
+ // With nothing stored at all there is no home to keep, so the stylesheet's
11763
+ // own corner is pinned as before.
11764
+ if (!boundsAreReal && pos !== null) return false;
11592
11765
  var at = pos;
11593
11766
  if (at === null) {
11594
11767
  var rect = rectOf(host);
@@ -11628,7 +11801,7 @@ function mountFloatPanel(scope, t, hostCtx) {
11628
11801
  at.y + shared.FLOAT_DOT <= bounds.bottom + 0.5
11629
11802
  );
11630
11803
  pos = { x: Math.round(at.x), y: Math.round(at.y) };
11631
- snapToCorner();
11804
+ snapToCorner(instant);
11632
11805
  // Both fields in ONE write: the dot is on the corner and the document is
11633
11806
  // told which corner that is, in the same `mutate`. A region that changed
11634
11807
  // shape is the caller that has to store the move it just made, wherever the
@@ -11653,8 +11826,22 @@ function mountFloatPanel(scope, t, hostCtx) {
11653
11826
  * `settleOnCorner`).
11654
11827
  */
11655
11828
  var reflowIntoBounds = function () {
11656
- if (!settleOnCorner(true)) return;
11657
- if (open) render();
11829
+ var bounds = floatBounds();
11830
+ var wasSide = cardQuadrant;
11831
+ if (pos !== null) {
11832
+ // The card's side belongs to the corner the dot is going to, so it is
11833
+ // decided BEFORE the dot is moved — the two then land in the same frame,
11834
+ // and a changed side never sweeps across the middle behind a slide.
11835
+ var home = cornerHome(bounds, pos);
11836
+ cardQuadrant = shared.floatCardCorner(bounds, home.x, home.y);
11837
+ }
11838
+ var moved = settleOnCorner(true);
11839
+ if (open && (moved || cardQuadrant !== wasSide)) {
11840
+ // The region moved the dot and possibly the card's side with it: follow it
11841
+ // in one motion rather than in a jump and a slide (see `anchorGlide`).
11842
+ anchorGlide = moved;
11843
+ render();
11844
+ }
11658
11845
  };
11659
11846
 
11660
11847
  /**
@@ -11948,7 +12135,10 @@ function mountFloatPanel(scope, t, hostCtx) {
11948
12135
  // browser cancelled) would otherwise unfold the card in the middle of the
11949
12136
  // conversation. An ordinary open — the dot already on its corner — moves
11950
12137
  // nothing at all.
11951
- settleOnCorner();
12138
+ settleOnCorner(false, true);
12139
+ // Which side the unfold goes to: the card's own vote, taken once, here —
12140
+ // the one moment before it exists on the page (see `voteCardQuadrant`).
12141
+ voteCardQuadrant();
11952
12142
  playEnter = true;
11953
12143
  if (closeTimer !== null) {
11954
12144
  globalThis.clearTimeout(closeTimer);
@@ -12242,10 +12432,35 @@ function mountFloatPanel(scope, t, hostCtx) {
12242
12432
  // region is where it lives from now on. Every other path reads the
12243
12433
  // remembered corner instead (see `settleOnCorner`), which is what keeps a
12244
12434
  // region change from re-deciding it from stale coordinates.
12245
- corner = shared.floatNearestCorner(floatBounds(), pos.x, pos.y);
12435
+ var bounds = floatBounds();
12436
+ // Which corner the dot goes to is read from the PANEL's own middle while
12437
+ // the panel is up — a panel dragged by its head hangs a full card away
12438
+ // from the dot, so asking where the DOT is made the user haul it far past
12439
+ // the conversation's own middle to reach the far side. The conversation's
12440
+ // middle is the origin either way: above it is up, right of it is right.
12441
+ var ref = cardMiddle();
12442
+ if (ref === null) {
12443
+ ref = { x: pos.x + shared.FLOAT_DOT / 2, y: pos.y + shared.FLOAT_DOT / 2 };
12444
+ }
12445
+ corner = shared.floatQuadrantIn(bounds, ref.x, ref.y);
12446
+ // The card's side is decided for the corner the dot LANDS on, not for
12447
+ // where the hand let go: a dot moved to the opposite corner has to take
12448
+ // the card with it, or the panel hangs off the conversation on the side
12449
+ // the old corner had room on.
12450
+ var home = shared.floatCornerPoint(bounds, corner);
12451
+ // Voted for the corner the dot is LANDING on, without moving the dot
12452
+ // first: a stand-in region refuses to move the dot, and a `pos` written
12453
+ // here would then claim a corner the host never went to.
12454
+ voteCardQuadrant(bounds, home);
12455
+ // The snap itself is always animated. The card re-anchors in the same
12456
+ // frame, so a side that changed is already on its new edge while the dot
12457
+ // slides — one reposition at the release point rather than a card dragged
12458
+ // across the screen with its far edge pinned.
12246
12459
  snapToCorner();
12247
12460
  var live = readState();
12248
12461
  writeLivePos(live);
12462
+ // The rebuild that follows rides the snap: a side that changed glides.
12463
+ anchorGlide = open;
12249
12464
  render();
12250
12465
  };
12251
12466
  var up = function () {
@@ -12457,7 +12672,12 @@ function mountFloatPanel(scope, t, hostCtx) {
12457
12672
  if (open && !(playEnter && !motionReduced()) && typeof dot.classList !== "undefined") {
12458
12673
  dot.classList.add("dfp-dotAway");
12459
12674
  }
12460
- if (!open) return;
12675
+ if (!open) {
12676
+ // A shut panel has no card to glide; a glide armed by a snap is spent here
12677
+ // rather than left to surprise the next open.
12678
+ anchorGlide = false;
12679
+ return;
12680
+ }
12461
12681
 
12462
12682
  // The shadow's own layer: the card is clipped while it animates, and a clip
12463
12683
  // on the card would clip the card's own filter away with it (see the sheet).
@@ -12483,15 +12703,24 @@ function mountFloatPanel(scope, t, hostCtx) {
12483
12703
  // first one is a decision, not a coincidence.
12484
12704
  anchor = { x: bounds.right, y: bounds.bottom };
12485
12705
  }
12486
- var quadrant = shared.floatQuadrantIn(bounds, anchor.x, anchor.y);
12487
- // The remembered corner is the answer the card is really anchored on: a
12488
- // region narrow enough puts a corner's coordinates on the other side of its
12489
- // own middle, and the card still has to grow AWAY from the corner the dot
12490
- // lives in rather than flip around it. A dot that is not on its remembered
12491
- // corner (mid-drag, or a position just adopted) is read from where it is.
12492
- // Asked through a helper: `corner` below is the CARD's anchored corner, and
12493
- // a local of that name here would shadow the dot's identity.
12494
- quadrant = dotQuadrant(bounds, anchor, quadrant);
12706
+ var quadrant;
12707
+ if (cardQuadrant !== null) {
12708
+ // The card's side is its own answer (see `voteCardQuadrant`): the side of
12709
+ // the dot that holds more of the card, voted when a side may change.
12710
+ quadrant = cardQuadrant;
12711
+ } else {
12712
+ // A card that has never been placed anywhere unfolds out of the dot, which
12713
+ // is also where a first panel ends up.
12714
+ quadrant = shared.floatQuadrantIn(bounds, anchor.x, anchor.y);
12715
+ // The remembered corner is the answer the card is really anchored on: a
12716
+ // region narrow enough puts a corner's coordinates on the other side of its
12717
+ // own middle, and the card still has to grow AWAY from the corner the dot
12718
+ // lives in rather than flip around it. A dot that is not on its remembered
12719
+ // corner (mid-drag, or a position just adopted) is read from where it is.
12720
+ // Asked through a helper: `corner` below is the CARD's anchored corner, and
12721
+ // a local of that name here would shadow the dot's identity.
12722
+ quadrant = dotQuadrant(bounds, anchor, quadrant);
12723
+ }
12495
12724
  liveQuadrant = quadrant;
12496
12725
  var expand = shared.floatExpandDirection(quadrant);
12497
12726
  try {
@@ -12822,10 +13051,45 @@ function mountFloatPanel(scope, t, hostCtx) {
12822
13051
  });
12823
13052
  card.appendChild(grip);
12824
13053
  })();
12825
- shadow.appendChild(el("div", "dfp-floatShade"));
13054
+ var shadeEl = el("div", "dfp-floatShade");
13055
+ shadeEl.style.transformOrigin = cardCorner.origin;
13056
+ shadow.appendChild(shadeEl);
12826
13057
  shadow.appendChild(glass);
12827
13058
  shadow.appendChild(card);
12828
13059
  host.appendChild(shadow);
13060
+ // A side is an anchored EDGE, so switching it moves the card by its own
13061
+ // width in one frame. On a snap that would be a jump to the middle of the
13062
+ // screen FOLLOWED by the slide to the corner — two motions. The card is put
13063
+ // back where it already was on screen with a translate, and that translate
13064
+ // is released over the dot's own 160 ms: the panel then travels from where
13065
+ // the user let go straight to the corner, in one motion, with the dot.
13066
+ var anchorLeft = 0;
13067
+ var anchorTop = 0;
13068
+ try {
13069
+ anchorLeft = shadow.offsetLeft;
13070
+ anchorTop = shadow.offsetTop;
13071
+ } catch (error) {
13072
+ // An unmeasurable layer keeps no glide; the side change is instant.
13073
+ }
13074
+ if (anchorGlide && lastAnchor !== null) {
13075
+ anchorGlide = false;
13076
+ var glideX = lastAnchor.left - anchorLeft;
13077
+ var glideY = lastAnchor.top - anchorTop;
13078
+ if (glideX !== 0 || glideY !== 0) {
13079
+ try {
13080
+ shadow.style.transform = "translate(" + glideX + "px," + glideY + "px)";
13081
+ // One forced layout commits that frame, so the next write has a value
13082
+ // to leave FROM; without it both writes land in one recalc and the
13083
+ // glide is the same jump it was meant to remove.
13084
+ shadow.getBoundingClientRect();
13085
+ shadow.style.transition = "transform " + FLOAT_SNAP_MS + "ms ease";
13086
+ shadow.style.transform = "none";
13087
+ } catch (error) {
13088
+ shadow.style.transform = "";
13089
+ }
13090
+ }
13091
+ }
13092
+ lastAnchor = { left: anchorLeft, top: anchorTop };
12829
13093
  // Enter animation, only on a real shut->open transition: the card starts
12830
13094
  // clipped to the dot's own rectangle, a hair smaller and dimmer, and the
12831
13095
  // next frames let the transition carry all three home (clip/transform/
package/lib/shared.cjs CHANGED
@@ -1185,6 +1185,15 @@ function floatBoundsPick(rects, viewport) {
1185
1185
  if (!isFinite(vh) || vh <= 0) vh = FLOAT_VIEWPORT_H;
1186
1186
  var full = { left: 0, top: 0, right: vw, bottom: vh, width: vw, height: vh };
1187
1187
  var list = Array.isArray(rects) ? rects : [];
1188
+ // A region narrower than one dot used to count as "no region at all", and the
1189
+ // fallback was the WHOLE viewport — so in a narrow window the dot parked on the
1190
+ // window's own corner, over the sidebar, instead of on the conversation's. The
1191
+ // largest readable box is kept as a fallback, and it names itself in `index` so
1192
+ // the watcher holds the box whose size actually defines the region. `full` is
1193
+ // left for a page with no region at all — or only degenerate 0-area rects.
1194
+ var best = null;
1195
+ var bestIndex = -1;
1196
+ var bestArea = -1;
1188
1197
  for (var index = 0; index < list.length; index += 1) {
1189
1198
  var box = floatBoxOf(list[index]);
1190
1199
  if (box === null) continue;
@@ -1194,12 +1203,18 @@ function floatBoundsPick(rects, viewport) {
1194
1203
  var bottom = Math.max(0, Math.min(vh, box.bottom));
1195
1204
  var width = right - left;
1196
1205
  var height = bottom - top;
1206
+ if (width > 0 && height > 0 && width * height > bestArea) {
1207
+ bestArea = width * height;
1208
+ bestIndex = index;
1209
+ best = { left: left, top: top, right: right, bottom: bottom, width: width, height: height };
1210
+ }
1197
1211
  if (width < FLOAT_BOUNDS_MIN || height < FLOAT_BOUNDS_MIN) continue;
1198
1212
  return {
1199
1213
  index: index,
1200
1214
  bounds: { left: left, top: top, right: right, bottom: bottom, width: width, height: height },
1201
1215
  };
1202
1216
  }
1217
+ if (best !== null) return { index: bestIndex, bounds: best };
1203
1218
  return { index: -1, bounds: full };
1204
1219
  }
1205
1220
 
@@ -1318,6 +1333,36 @@ function floatTransformOrigin(quadrant) {
1318
1333
  return "100% 100%";
1319
1334
  }
1320
1335
 
1336
+ /**
1337
+ * Which side the open CARD grows towards: the one with more room for it.
1338
+ *
1339
+ * The card hangs off the dot, so it can only extend one way from it, and the
1340
+ * side that holds more of the card is the side the conversation has room for.
1341
+ * Each axis compares the room beyond the dot's own box on either side and sends
1342
+ * the card to the larger one — which is also the side its middle ends up on, so
1343
+ * "the panel goes to the corner it sits in" holds by construction.
1344
+ *
1345
+ * This is the CARD's rule, not the dot's: the dot keeps the corner it remembers
1346
+ * living in (see `floatCornerPoint`), while a rule shared by both is what made
1347
+ * the card jump whenever the dot crossed a line the card had not. The answer
1348
+ * depends on the dot and the region and on nothing else — never on a card it
1349
+ * just placed — so it cannot vote itself back and forth.
1350
+ * @param {unknown} bounds - the region, as `floatBoundsFrom` returns it.
1351
+ * @param {number} x - the dot's left edge, in CSS pixels.
1352
+ * @param {number} y - the dot's top edge, in CSS pixels.
1353
+ * @param {number} [dot] - the dot's diameter (defaults to FLOAT_DOT).
1354
+ * @returns {string} one of "tl", "tr", "bl", "br" — the quadrant the card is
1355
+ * anchored on, which is the corner it grows away from.
1356
+ */
1357
+ function floatCardCorner(bounds, x, y, dot) {
1358
+ var box = floatBoxOf(bounds);
1359
+ if (box === null) box = { left: 0, top: 0, right: FLOAT_VIEWPORT_W, bottom: FLOAT_VIEWPORT_H };
1360
+ var size = dot === undefined ? FLOAT_DOT : dot;
1361
+ var growRight = box.right - x >= x + size - box.left;
1362
+ var growDown = box.bottom - y >= y + size - box.top;
1363
+ return (growDown ? "t" : "b") + (growRight ? "l" : "r");
1364
+ }
1365
+
1321
1366
  /**
1322
1367
  * Which corner of the card lands on the dot, and where the expansion starts.
1323
1368
  *
@@ -4614,6 +4659,7 @@ var shared = {
4614
4659
  floatExpandDirection: floatExpandDirection,
4615
4660
  floatTransformOrigin: floatTransformOrigin,
4616
4661
  floatCardAnchor: floatCardAnchor,
4662
+ floatCardCorner: floatCardCorner,
4617
4663
  floatClipStart: floatClipStart,
4618
4664
  floatClipRest: floatClipRest,
4619
4665
  isRestClip: isRestClip,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
5
5
  "type": "module",
6
6
  "icon": "icon.svg",