dsh-fonttune 0.7.4 → 0.7.5

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,11 @@
2
2
 
3
3
  All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
4
4
 
5
+ ## [0.7.5] - 2026-11-01
6
+
7
+ ### Fixed
8
+ - **The floating dot covered the composer in a new session**: before conversation text appeared, the dot could remain over the input area; it now moves to the lower-left of the main content area, clear of both the composer and the sidebar.
9
+
5
10
  ## [0.7.4] - 2026-10-30
6
11
 
7
12
  ### Fixed
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  **dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
4
4
 
5
+ ## [0.7.5] - 2026-11-01
6
+
7
+ ### 修复
8
+ - **新会话中的悬浮圆点遮挡输入框**:对话正文尚未出现时,圆点可能停在输入框内;现在会将这类位置移至主内容区左下角,避开输入框与侧栏。
9
+
5
10
  ## [0.7.4] - 2026-10-30
6
11
 
7
12
  ### 修复
package/lib/client.js CHANGED
@@ -1238,6 +1238,23 @@ function floatBoundsFrom(rects, viewport) {
1238
1238
  return floatBoundsPick(rects, viewport).bounds;
1239
1239
  }
1240
1240
 
1241
+ /**
1242
+ * The fallback region to use when the conversation has no measurable text yet.
1243
+ *
1244
+ * The page's whole-window stand-in starts at x=0, which puts the dot over the left
1245
+ * navigation rail. On a host shell that exposes a main-column rectangle, using that
1246
+ * rectangle as the fallback puts the dot at the content column's left corner while
1247
+ * the conversation itself is mounting. If the shell supplies no usable region, the
1248
+ * viewport stays the fallback.
1249
+ * @param {unknown} region - the main content column, or null when unavailable.
1250
+ * @param {{width: number, height: number}} viewport - the window size.
1251
+ * @returns {{left:number,top:number,right:number,bottom:number,width:number,height:number}}
1252
+ * the clipped content box, or the whole viewport.
1253
+ */
1254
+ function floatFallbackBounds(region, viewport) {
1255
+ return floatBoundsPick(region === null || region === undefined ? [] : [region], viewport).bounds;
1256
+ }
1257
+
1241
1258
  /**
1242
1259
  * Whether a second reading of the winning rectangle is the same shape as the last.
1243
1260
  *
@@ -4966,6 +4983,7 @@ var shared = {
4966
4983
  floatBoxOf: floatBoxOf,
4967
4984
  floatBoundsPick: floatBoundsPick,
4968
4985
  floatBoundsFrom: floatBoundsFrom,
4986
+ floatFallbackBounds: floatFallbackBounds,
4969
4987
  sameWatchBounds: sameWatchBounds,
4970
4988
  floatTargetsConnected: floatTargetsConnected,
4971
4989
  floatQuadrantIn: floatQuadrantIn,
@@ -11361,9 +11379,9 @@ function mountFloatPanel(scope, t, hostCtx) {
11361
11379
  return null;
11362
11380
  };
11363
11381
 
11364
- // Whether the last region reading was a real region or the whole-window
11365
- // stand-in (see `floatBounds`). A stand-in never moves the dot: it means "no
11366
- // conversation to measure right now", not "the conversation is the window".
11382
+ // Whether the last bounds reading was a REAL conversation region or a stand-in
11383
+ // (main column, then viewport; see `floatBounds`). A stand-in means "no conversation
11384
+ // to measure right now", not "the conversation is that whole shell column".
11367
11385
  var boundsAreReal = false;
11368
11386
 
11369
11387
  /** The window size the bounds are cut to, with the shared fallback. */
@@ -11381,9 +11399,62 @@ function mountFloatPanel(scope, t, hostCtx) {
11381
11399
  * session had 36. The first in document order is usually an old turn scrolled
11382
11400
  * thousands of pixels off screen, and its scroll container can measure zero;
11383
11401
  * reading the bounds from that one element alone threw both of its candidates
11384
- * away and snapped the dot to the whole window instead of the conversation.
11402
+ * away and snapped the dot to the fallback area instead of the conversation.
11385
11403
  * @returns {Array<object>} the prose elements (possibly empty).
11386
11404
  */
11405
+ var EDITABLE_ANCESTOR =
11406
+ "[contenteditable]:not([contenteditable='false']),textarea,input,[role='textbox']";
11407
+
11408
+ /**
11409
+ * Whether a node sits inside something the reader types into.
11410
+ * @param {object} node - the element to test (it counts as its own ancestor).
11411
+ * @returns {boolean} true for the composer and anything inside it.
11412
+ */
11413
+ var insideEditable = function (node) {
11414
+ try {
11415
+ return node.closest(EDITABLE_ANCESTOR) !== null;
11416
+ } catch (error) {
11417
+ return false;
11418
+ }
11419
+ };
11420
+
11421
+ /**
11422
+ * Whether the dot's stored position lies over a field the reader types into.
11423
+ *
11424
+ * On a fresh session no conversation region is measurable, and a stand-in never
11425
+ * moves a dot that already has a position. A dot saved over the composer (the
11426
+ * reported bug: its region was the input's own box) would therefore stay inside
11427
+ * the input for good. That place is wrong whatever the region is, so this is the
11428
+ * one case the stand-in may relocate.
11429
+ * @returns {boolean} true when the stored square overlaps an editable field.
11430
+ */
11431
+ var strandedInField = function () {
11432
+ if (pos === null) return false;
11433
+ var left = pos.x;
11434
+ var top = pos.y;
11435
+ var right = pos.x + shared.FLOAT_DOT;
11436
+ var bottom = pos.y + shared.FLOAT_DOT;
11437
+ try {
11438
+ var fields = document.querySelectorAll(EDITABLE_ANCESTOR);
11439
+ for (var index = 0; index < fields.length; index += 1) {
11440
+ var box = fields[index].getBoundingClientRect();
11441
+ if (
11442
+ box.width > 0 &&
11443
+ box.height > 0 &&
11444
+ left < box.right &&
11445
+ right > box.left &&
11446
+ top < box.bottom &&
11447
+ bottom > box.top
11448
+ ) {
11449
+ return true;
11450
+ }
11451
+ }
11452
+ } catch (error) {
11453
+ return false;
11454
+ }
11455
+ return false;
11456
+ };
11457
+
11387
11458
  var proseElements = function () {
11388
11459
  var found = [];
11389
11460
  var add = function (nodes) {
@@ -11398,7 +11469,17 @@ function mountFloatPanel(scope, t, hostCtx) {
11398
11469
  } catch (error) {
11399
11470
  // A page without a queryable document keeps the empty list (the window).
11400
11471
  }
11401
- return found;
11472
+ // The composer is part of the conversation container on a fresh session, so a
11473
+ // paragraph the input holds matches the conversation selector too. Measured
11474
+ // live: one typed character left a `<p>` in the input, and the dot's region
11475
+ // became the input's own scroll box, which parked the dot inside the input. A
11476
+ // message is never typed into a field, so anything inside one is not the
11477
+ // conversation, whatever class or attribute it carries.
11478
+ var kept = [];
11479
+ for (var at = 0; at < found.length; at += 1) {
11480
+ if (!insideEditable(found[at])) kept.push(found[at]);
11481
+ }
11482
+ return kept;
11402
11483
  };
11403
11484
 
11404
11485
  /**
@@ -11490,25 +11571,71 @@ function mountFloatPanel(scope, t, hostCtx) {
11490
11571
  return true;
11491
11572
  };
11492
11573
 
11574
+ /**
11575
+ * The main content column while the conversation itself has no measurable prose.
11576
+ *
11577
+ * DSH's shell marks its bottom-row element with a stable data attribute and puts
11578
+ * it directly inside the three-column frame. The frame's direct children are the
11579
+ * left rail, the main column and the right rail; taking the main column's actual
11580
+ * rectangle means its left edge follows an open or collapsed sidebar without a
11581
+ * plugin-specific width guess, and its right edge leaves room for the right rail.
11582
+ * If the shell changes shape or is not present, the viewport remains the fallback.
11583
+ * @returns {object|null} the main column's rectangle, or null when it is not usable.
11584
+ */
11585
+ var shellMainColumnRect = function () {
11586
+ try {
11587
+ var bottom = document.querySelector("[data-shell-bottom]");
11588
+ var frame = bottom === null ? null : bottom.parentElement;
11589
+ if (frame === null || frame === undefined || frame.children.length < 2) return null;
11590
+ var sidebar = frame.children[0];
11591
+ var main = frame.children[1];
11592
+ if (
11593
+ main === null ||
11594
+ main === undefined ||
11595
+ main.hasAttribute("data-rightbar-col") ||
11596
+ main.hasAttribute("data-shell-bottom") ||
11597
+ main.hasAttribute("data-shell-overlay")
11598
+ ) {
11599
+ return null;
11600
+ }
11601
+ var sidebarRect = rectOf(sidebar);
11602
+ var mainRect = rectOf(main);
11603
+ if (
11604
+ sidebarRect === null ||
11605
+ mainRect === null ||
11606
+ !(sidebarRect.width > 0) ||
11607
+ !(mainRect.width > 0) ||
11608
+ !(mainRect.height > 0) ||
11609
+ mainRect.left + 1 < sidebarRect.right
11610
+ ) {
11611
+ return null;
11612
+ }
11613
+ return mainRect;
11614
+ } catch (error) {
11615
+ return null;
11616
+ }
11617
+ };
11618
+
11493
11619
  /**
11494
11620
  * The rectangle the dot snaps to, drags within and reads its own quadrant from.
11495
11621
  *
11496
- * The conversation when it can be found, the whole window when it cannot (a
11497
- * settings page has no prose at all): the window is what this used to be
11498
- * everywhere, and it stays the last resort rather than the rule.
11622
+ * The conversation when it can be found; when no conversation is measurable,
11623
+ * the host's main column stands in so the bottom-left home is just past the left
11624
+ * rail, rather than underneath its avatar. On pages that do not expose a main
11625
+ * column, the whole window remains the fallback.
11499
11626
  *
11500
- * WHICH one answered is remembered in `boundsAreReal`, because the two are not
11501
- * interchangeable for a MOVE: the whole window is a stand-in for "there is no
11502
- * conversation to measure at this instant" — a session still mounting, a
11503
- * message list laid out at 0×0 — and a dot parked on the window's corner over
11504
- * a page that does have a conversation is exactly the reported "it sits on the
11505
- * window's left edge instead of the conversation's corner".
11627
+ * WHICH one answered is remembered in `boundsAreReal`, because the main column
11628
+ * is still a stand-in, not a real conversation region: a stored dot only moves
11629
+ * within it when its saved position sits over an editable field (see
11630
+ * `strandedInField`).
11506
11631
  * @returns {object} the bounds, in viewport coordinates.
11507
11632
  */
11508
11633
  var floatBounds = function () {
11509
- var picked = shared.floatBoundsPick(floatBoundsRects(), floatViewport());
11634
+ var viewport = floatViewport();
11635
+ var picked = shared.floatBoundsPick(floatBoundsRects(), viewport);
11510
11636
  boundsAreReal = picked.index >= 0;
11511
- return picked.bounds;
11637
+ if (boundsAreReal) return picked.bounds;
11638
+ return shared.floatFallbackBounds(shellMainColumnRect(), viewport);
11512
11639
  };
11513
11640
 
11514
11641
  /**
@@ -12064,13 +12191,12 @@ function mountFloatPanel(scope, t, hostCtx) {
12064
12191
  */
12065
12192
  var snapToCorner = function (instant) {
12066
12193
  var bounds = floatBounds();
12067
- // A stand-in region is not a home: the window's corner is what the dot would
12068
- // land on over a page whose conversation is merely not measurable yet, which
12069
- // is the reported "it snaps to the window's left edge instead of the
12070
- // conversation's corner". A dot the document already placed keeps the corner
12194
+ // A stand-in region is not a home: the main-column corner is only where an
12195
+ // editable-field position can be re-homed while the conversation is not
12196
+ // measurable yet. A dot the document already placed keeps the corner
12071
12197
  // the last REAL region gave it; the observer settles it again the moment one
12072
12198
  // exists (see `reflowIntoBounds`).
12073
- if (!boundsAreReal && pos !== null) return pos;
12199
+ if (!boundsAreReal && pos !== null && !strandedInField()) return pos;
12074
12200
  var snapped;
12075
12201
  if (pos !== null) {
12076
12202
  snapped = cornerHome(bounds, pos);
@@ -12212,10 +12338,13 @@ function mountFloatPanel(scope, t, hostCtx) {
12212
12338
  var bounds = floatBounds();
12213
12339
  // Nothing real to settle onto: a shut panel on a settings page, or a
12214
12340
  // conversation that is not measurable at this instant, leaves the dot where
12215
- // it is — moving it onto the window's corner is the bug, not the fallback.
12341
+ // it is — moving it onto the fallback corner is the bug, not the fallback.
12216
12342
  // With nothing stored at all there is no home to keep, so the stylesheet's
12217
- // own corner is pinned as before.
12218
- if (!boundsAreReal && pos !== null) return false;
12343
+ // own corner is pinned as before. The one exception is a dot stored INSIDE a
12344
+ // field the reader types into (see `strandedInField`): that place is wrong
12345
+ // whatever the region is, so it is re-homed to the main column and persisted.
12346
+ var rehomeEditable = !boundsAreReal && pos !== null && strandedInField();
12347
+ if (!boundsAreReal && pos !== null && !rehomeEditable) return false;
12219
12348
  var at = pos;
12220
12349
  if (at === null) {
12221
12350
  var rect = rectOf(host);
@@ -12260,7 +12389,7 @@ function mountFloatPanel(scope, t, hostCtx) {
12260
12389
  // told which corner that is, in the same `mutate`. A region that changed
12261
12390
  // shape is the caller that has to store the move it just made, wherever the
12262
12391
  // dot was standing before (see the note above).
12263
- if (outside || naming) persistIfDirty();
12392
+ if (outside || naming || rehomeEditable) persistIfDirty();
12264
12393
  if (regionMoved === true) persistIfDirty();
12265
12394
  return true;
12266
12395
  };
package/lib/shared.cjs CHANGED
@@ -1229,6 +1229,23 @@ function floatBoundsFrom(rects, viewport) {
1229
1229
  return floatBoundsPick(rects, viewport).bounds;
1230
1230
  }
1231
1231
 
1232
+ /**
1233
+ * The fallback region to use when the conversation has no measurable text yet.
1234
+ *
1235
+ * The page's whole-window stand-in starts at x=0, which puts the dot over the left
1236
+ * navigation rail. On a host shell that exposes a main-column rectangle, using that
1237
+ * rectangle as the fallback puts the dot at the content column's left corner while
1238
+ * the conversation itself is mounting. If the shell supplies no usable region, the
1239
+ * viewport stays the fallback.
1240
+ * @param {unknown} region - the main content column, or null when unavailable.
1241
+ * @param {{width: number, height: number}} viewport - the window size.
1242
+ * @returns {{left:number,top:number,right:number,bottom:number,width:number,height:number}}
1243
+ * the clipped content box, or the whole viewport.
1244
+ */
1245
+ function floatFallbackBounds(region, viewport) {
1246
+ return floatBoundsPick(region === null || region === undefined ? [] : [region], viewport).bounds;
1247
+ }
1248
+
1232
1249
  /**
1233
1250
  * Whether a second reading of the winning rectangle is the same shape as the last.
1234
1251
  *
@@ -4957,6 +4974,7 @@ var shared = {
4957
4974
  floatBoxOf: floatBoxOf,
4958
4975
  floatBoundsPick: floatBoundsPick,
4959
4976
  floatBoundsFrom: floatBoundsFrom,
4977
+ floatFallbackBounds: floatFallbackBounds,
4960
4978
  sameWatchBounds: sameWatchBounds,
4961
4979
  floatTargetsConnected: floatTargetsConnected,
4962
4980
  floatQuadrantIn: floatQuadrantIn,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
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",