dsh-fonttune 0.6.2 → 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 +24 -0
- package/CHANGELOG.md +24 -0
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/docs/panel.jpg +0 -0
- package/icon.svg +21 -0
- package/lib/client.js +892 -89
- package/lib/shared.cjs +89 -0
- package/package.json +3 -1
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
|
*
|
|
@@ -1853,6 +1898,47 @@ function normalizeDarkValues(value, relative) {
|
|
|
1853
1898
|
/** The three axes that share the weight scale (and its marker). */
|
|
1854
1899
|
var WEIGHT_FIELDS = [WEIGHT_FIELD, WEIGHT_DIALOG_FIELD, CODE_WEIGHT_FIELD];
|
|
1855
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* Return a copy of a sparse dark-theme override map with one field changed.
|
|
1903
|
+
*
|
|
1904
|
+
* The map holds only the fields the reader overrode for the dark theme; an
|
|
1905
|
+
* absent field inherits the light value. `undefined` therefore means "stop
|
|
1906
|
+
* overriding", not "set to empty" — that distinction is what lets the card show
|
|
1907
|
+
* "follows the light set" instead of a value the reader never chose.
|
|
1908
|
+
* @param {unknown} map - the current sparse map (anything object-shaped).
|
|
1909
|
+
* @param {string} field - a settings field name.
|
|
1910
|
+
* @param {string|number|boolean|undefined} value - the override, or undefined to clear it.
|
|
1911
|
+
* @returns {Record<string, string|number|boolean>} a new map; the input is untouched.
|
|
1912
|
+
*/
|
|
1913
|
+
function withDarkOverride(map, field, value) {
|
|
1914
|
+
var source = map !== null && typeof map === "object" && !Array.isArray(map) ? map : {};
|
|
1915
|
+
var out = {};
|
|
1916
|
+
var key;
|
|
1917
|
+
for (key in source) {
|
|
1918
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) out[key] = source[key];
|
|
1919
|
+
}
|
|
1920
|
+
if (value === undefined) delete out[field];
|
|
1921
|
+
else out[field] = value;
|
|
1922
|
+
return out;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* Serialize a dark override map for storage, with its keys in a stable order.
|
|
1927
|
+
*
|
|
1928
|
+
* Stable order matters twice: the settings document is diffed as text (an
|
|
1929
|
+
* equivalent map written in a different key order would look like a change and
|
|
1930
|
+
* cost a whole-document write), and the preset snapshot compares the text too.
|
|
1931
|
+
* @param {unknown} map - the sparse map.
|
|
1932
|
+
* @returns {string} the JSON text the document stores.
|
|
1933
|
+
*/
|
|
1934
|
+
function darkValuesText(map) {
|
|
1935
|
+
var source = map !== null && typeof map === "object" && !Array.isArray(map) ? map : {};
|
|
1936
|
+
var keys = Object.keys(source).sort();
|
|
1937
|
+
var out = {};
|
|
1938
|
+
for (var index = 0; index < keys.length; index += 1) out[keys[index]] = source[keys[index]];
|
|
1939
|
+
return JSON.stringify(out);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1856
1942
|
/**
|
|
1857
1943
|
* Convert the weight fields of saved preset snapshots to offsets.
|
|
1858
1944
|
*
|
|
@@ -4582,6 +4668,7 @@ var shared = {
|
|
|
4582
4668
|
floatExpandDirection: floatExpandDirection,
|
|
4583
4669
|
floatTransformOrigin: floatTransformOrigin,
|
|
4584
4670
|
floatCardAnchor: floatCardAnchor,
|
|
4671
|
+
floatCardCorner: floatCardCorner,
|
|
4585
4672
|
floatClipStart: floatClipStart,
|
|
4586
4673
|
floatClipRest: floatClipRest,
|
|
4587
4674
|
isRestClip: isRestClip,
|
|
@@ -4685,6 +4772,8 @@ var shared = {
|
|
|
4685
4772
|
quoteFamily: quoteFamily,
|
|
4686
4773
|
normalizeConfig: normalizeConfig,
|
|
4687
4774
|
normalizeDarkValues: normalizeDarkValues,
|
|
4775
|
+
withDarkOverride: withDarkOverride,
|
|
4776
|
+
darkValuesText: darkValuesText,
|
|
4688
4777
|
normalizePresets: normalizePresets,
|
|
4689
4778
|
normalizeValueSet: normalizeValueSet,
|
|
4690
4779
|
clampOffset: clampOffset,
|
|
@@ -5188,6 +5277,15 @@ var DICTS = {
|
|
|
5188
5277
|
"preset.import": "Import",
|
|
5189
5278
|
"preset.importPlaceholder": "Paste an exported preset JSON here",
|
|
5190
5279
|
"preset.label": "Presets",
|
|
5280
|
+
"theme.label": "Light/dark sync",
|
|
5281
|
+
"theme.surface": "Which theme is being edited",
|
|
5282
|
+
"theme.shared": "One set",
|
|
5283
|
+
"theme.split": "Separate",
|
|
5284
|
+
"theme.light": "Light",
|
|
5285
|
+
"theme.dark": "Dark",
|
|
5286
|
+
"theme.sharedHint": "Light and dark share this one set of settings.",
|
|
5287
|
+
"theme.splitHint": "Editing the dark set: only the items you change are overridden, the rest follow the light set.",
|
|
5288
|
+
"theme.darkOnly": "Dark",
|
|
5191
5289
|
"preset.select": "Choose a preset",
|
|
5192
5290
|
"preset.rename": "Rename",
|
|
5193
5291
|
"preset.renamePlaceholder": "New name",
|
|
@@ -5378,6 +5476,15 @@ var DICTS = {
|
|
|
5378
5476
|
"preset.import": "导入",
|
|
5379
5477
|
"preset.importPlaceholder": "粘贴导出的方案内容",
|
|
5380
5478
|
"preset.label": "预设方案",
|
|
5479
|
+
"theme.label": "亮暗同步",
|
|
5480
|
+
"theme.surface": "正在编辑哪一套",
|
|
5481
|
+
"theme.shared": "一起设置",
|
|
5482
|
+
"theme.split": "亮暗分开",
|
|
5483
|
+
"theme.light": "亮色",
|
|
5484
|
+
"theme.dark": "暗色",
|
|
5485
|
+
"theme.sharedHint": "亮色与暗色共用同一套排印设置。",
|
|
5486
|
+
"theme.splitHint": "正在编辑暗色那一套:只覆盖你调过的项,其余跟随亮色。",
|
|
5487
|
+
"theme.darkOnly": "暗色",
|
|
5381
5488
|
"preset.select": "选择方案",
|
|
5382
5489
|
"preset.rename": "改名",
|
|
5383
5490
|
"preset.renamePlaceholder": "新名字",
|
|
@@ -5514,6 +5621,14 @@ var CARD_CSS = [
|
|
|
5514
5621
|
".dfp-miniButton{appearance:none;font:inherit;cursor:pointer;white-space:nowrap;flex:none;border-radius:8px;padding:3px 8px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);background:0 0;border:.5px solid var(--dsw-alias-border-l3)}",
|
|
5515
5622
|
".dfp-miniButton:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-specific-sidebar-nav-item-hover)}",
|
|
5516
5623
|
".dfp-miniButton:disabled{opacity:.4;cursor:default}",
|
|
5624
|
+
// The theme surface picker: always mounted, so turning the sync switch on and
|
|
5625
|
+
// off slides and fades this row instead of snapping the card's height. The grid
|
|
5626
|
+
// row is the height animation (0fr -> 1fr), the opacity rides along.
|
|
5627
|
+
".dfp-themeSplit{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .24s ease,opacity .24s ease}",
|
|
5628
|
+
".dfp-themeSplitOn{grid-template-rows:1fr;opacity:1}",
|
|
5629
|
+
".dfp-themeSplitClip{min-height:0;overflow:hidden}",
|
|
5630
|
+
".dfp-themeSplitRow{display:flex;align-items:center;gap:10px;margin-top:8px}",
|
|
5631
|
+
".dfp-themeNote{flex:1;min-width:0;text-align:right;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
5517
5632
|
".dfp-textInput{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;font:inherit;font-size:13px;line-height:18px}",
|
|
5518
5633
|
// The auto-save hint and the transient message share one row: the hint holds
|
|
5519
5634
|
// the left, the message fades in on the right. The row is always mounted and
|
|
@@ -5615,7 +5730,7 @@ var CARD_CSS = [
|
|
|
5615
5730
|
// the dot coming back: the whole fold, not the 120 ms the way out took. The
|
|
5616
5731
|
// after-change transition is the one that counts, so this rule alone decides
|
|
5617
5732
|
// the fade-in, and it is the fold's own length and curve.
|
|
5618
|
-
".dfp-dotBack{transition:opacity .
|
|
5733
|
+
".dfp-dotBack{transition:opacity .46s cubic-bezier(.45,0,.25,1),transform .2s ease}",
|
|
5619
5734
|
// The shadow is a FILTER, not a box-shadow: a box-shadow is painted outside
|
|
5620
5735
|
// the border box, so every frame that carries a clip (both animations) would
|
|
5621
5736
|
// cut it away.
|
|
@@ -5629,12 +5744,57 @@ var CARD_CSS = [
|
|
|
5629
5744
|
// filter is applied to the card's already-clipped shape and follows it frame
|
|
5630
5745
|
// by frame. It is a pure paint layer: nothing about it takes a pointer, and
|
|
5631
5746
|
// the card it holds keeps every part of the animation.
|
|
5632
|
-
|
|
5747
|
+
// No `filter` here any more: an ancestor that filters is a backdrop root, and
|
|
5748
|
+
// the glass layer's blur would then sample only this wrapper's own subtree —
|
|
5749
|
+
// i.e. the panel itself — instead of the conversation behind it. The drop
|
|
5750
|
+
// shadow moved onto the glass, which is the layer that actually paints now.
|
|
5751
|
+
".dfp-floatShadow{position:absolute;z-index:1;box-sizing:border-box;pointer-events:none;width:min(264px,calc(100vw - 32px))}",
|
|
5633
5752
|
// The card fills the wrapper in normal flow, so the wrapper's anchored corner
|
|
5634
5753
|
// IS the card's corner: the clip, the scale and the fade still ride the card
|
|
5635
5754
|
// (and every measurement still reads it), while the anchored edge, the size
|
|
5636
5755
|
// and the shadow belong to the wrapper around it.
|
|
5637
|
-
".dfp-floatCard{position:relative;box-sizing:border-box;display:flex;flex-direction:column;width:100%;height:100%;max-height:calc(100vh - 24px);overflow:hidden;background:
|
|
5756
|
+
".dfp-floatCard{position:relative;z-index:1;box-sizing:border-box;display:flex;flex-direction:column;width:100%;height:100%;max-height:calc(100vh - 24px);overflow:hidden;background:transparent;border:1px solid var(--dsw-alias-border-l3);border-radius:16px;padding:10px 12px 12px;pointer-events:auto;clip-path:inset(0px 0px 0px 0px round 16px);transition:clip-path .32s cubic-bezier(.45,0,.25,1),opacity .32s cubic-bezier(.45,0,.25,1),transform .32s cubic-bezier(.45,0,.25,1)}",
|
|
5757
|
+
// The drop shadow, on a sibling of the card: it must NOT sit on an ancestor of
|
|
5758
|
+
// the blur layer (`filter` makes an ancestor a backdrop root, and the blur would
|
|
5759
|
+
// then sample the panel itself), and it must not sit on the card (Chromium carves
|
|
5760
|
+
// an element's own shadow away with its `clip-path` for the whole unfold).
|
|
5761
|
+
// It FADES with the unfold rather than mirroring the clip: two independent clip
|
|
5762
|
+
// transitions get retargeted a frame apart by Chrome (measured: 73 ms against the
|
|
5763
|
+
// card's 280 ms), and a shadow that outran the panel it belongs to reads worse
|
|
5764
|
+
// than one that arrives with it. Invisible while the card is still a dot, and
|
|
5765
|
+
// gone before the element is dropped — never a bare frame left hanging in the
|
|
5766
|
+
// conversation, which is exactly what a full-size, never-animated shadow layer
|
|
5767
|
+
// looked like.
|
|
5768
|
+
".dfp-floatShade{position:absolute;inset:0;z-index:0;border-radius:16px;pointer-events:none;box-shadow:0 10px 32px rgba(0,0,0,.28);opacity:1;transition:opacity .32s cubic-bezier(.45,0,.25,1)}",
|
|
5769
|
+
// The surface: a CHILD of the card, so the card's own clip gives it the panel's
|
|
5770
|
+
// silhouette for free — the unfold, the fold and every future shape change are
|
|
5771
|
+
// the card's business, not another layer's to keep in step. (A sibling was tried
|
|
5772
|
+
// first: two independent clip transitions, retargeted a frame apart by Chrome,
|
|
5773
|
+
// made the surface race ahead of the panel it was painting behind.)
|
|
5774
|
+
// It sits at z-index -1 so it paints under the rows, and it must never be given
|
|
5775
|
+
// opacity below 1 — that would make it a backdrop root and switch off its own
|
|
5776
|
+
// blur.
|
|
5777
|
+
// A SIBLING of the card, not a child. Measured: inside the card the blur is
|
|
5778
|
+
// computed but has no effect — the text behind the panel stayed crisp — because
|
|
5779
|
+
// a backdrop-filter inside the clipped, animated wrapper samples that wrapper's
|
|
5780
|
+
// own subtree instead of the page. quick-toc reached the same conclusion the hard
|
|
5781
|
+
// way and put its twin at the shell level for exactly this reason.
|
|
5782
|
+
// Its clip is copied from the card every frame by `mirrorGlassClip`: two
|
|
5783
|
+
// independent CSS clip transitions get retargeted a frame apart (measured 73 ms
|
|
5784
|
+
// against the card's 280 ms), and a surface that outruns the panel it is painting
|
|
5785
|
+
// behind is precisely the "transparent box grows first" artefact.
|
|
5786
|
+
".dfp-floatGlass{position:absolute;inset:0;z-index:0;border-radius:16px;pointer-events:none;background:var(--dsw-alias-bg-layer-2);clip-path:inset(0px 0px 0px 0px round 16px)}",
|
|
5787
|
+
// 82 % leaves the blurred conversation readable as colour and light behind the
|
|
5788
|
+
// rows, which is where quick-toc landed for a surface you read IN (its curtain,
|
|
5789
|
+
// meant to be seen through, goes lighter still).
|
|
5790
|
+
// Plain `rgba`, no `color-mix`. The mix version followed the theme's own surface
|
|
5791
|
+
// colour, but a browser without it drops the declaration outright and the opaque
|
|
5792
|
+
// fill underneath wins — "still opaque, and no blur visible" is exactly what that
|
|
5793
|
+
// looks like. A fixed 62 % of white (light) or of the dark surface (below) is the
|
|
5794
|
+
// same material in every browser, which is worth more here than matching a custom
|
|
5795
|
+
// palette exactly.
|
|
5796
|
+
".dfp-floatGlassOn{background:rgba(255,255,255,0.50);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}",
|
|
5797
|
+
"body[data-ds-dark-theme] .dfp-floatGlassOn{background:rgba(26,28,34,0.50)}",
|
|
5638
5798
|
// The body owns the scrolling: seven sliders and the status block do not fit a panel
|
|
5639
5799
|
// that was resized to its minimum, and a clipped slider is worse than a scrollbar.
|
|
5640
5800
|
".dfp-floatHead{flex:0 0 auto}",
|
|
@@ -5643,14 +5803,27 @@ var CARD_CSS = [
|
|
|
5643
5803
|
// smaller and dimmer, and the transition (started by dropping this class two
|
|
5644
5804
|
// frames later) carries all three home at once. Once it has arrived the card
|
|
5645
5805
|
// drops the clip entirely (see `releaseClip` in the client).
|
|
5646
|
-
".dfp-floatCardEnter{clip-path:var(--dfp-clipStart,inset(0px 0px 0px 0px round 16px));
|
|
5806
|
+
".dfp-floatCardEnter{clip-path:var(--dfp-clipStart,inset(0px 0px 0px 0px round 16px));transform:scale(.985)}",
|
|
5647
5807
|
// The leave frame: the same rectangle the enter came from, so the fold is the
|
|
5648
5808
|
// expand played backwards — but on its OWN, longer curve. The unfold is a
|
|
5649
5809
|
// fast, eager ease-out; the fold starts gently and spends most of its extra
|
|
5650
5810
|
// time in the tail, which is what makes shutting down read as deliberate
|
|
5651
5811
|
// instead of abrupt. The card is already invisible but must not swallow
|
|
5652
5812
|
// clicks aimed at the page underneath while it is still in the DOM.
|
|
5653
|
-
".dfp-floatCardLeave{clip-path:var(--dfp-clipStart,inset(0px 0px 0px 0px round 16px));opacity:0;transform:scale(.985);pointer-events:none;transition:clip-path .
|
|
5813
|
+
".dfp-floatCardLeave{clip-path:var(--dfp-clipStart,inset(0px 0px 0px 0px round 16px));opacity:0;transform:scale(.985);pointer-events:none;transition:clip-path .46s cubic-bezier(.45,0,.25,1),opacity .46s cubic-bezier(.45,0,.25,1),transform .46s cubic-bezier(.45,0,.25,1)}",
|
|
5814
|
+
// The shade's own two frames, AFTER the card's so a bare `.dfp-floatCardEnter`
|
|
5815
|
+
// lookup still finds the card's rule first. Opacity only: the shade has to leave
|
|
5816
|
+
// and arrive with the panel, but it must not mirror the clip (see above).
|
|
5817
|
+
//
|
|
5818
|
+
// It does NOT hang off the enter class: that class is the ARMING frame and is
|
|
5819
|
+
// dropped two frames in (see `settle`), so a shade tied to it blinked out and
|
|
5820
|
+
// straight back on while the card was still a dot. It is pinned to 0 for those
|
|
5821
|
+
// two frames instead — so the 0 has painted before the transition starts — and
|
|
5822
|
+
// then released, which fades it in over the very same 280 ms the card takes to
|
|
5823
|
+
// grow. The shadow therefore arrives exactly with the panel: not before the card
|
|
5824
|
+
// is there, and not as a separate appearance after it has landed.
|
|
5825
|
+
".dfp-floatShade.dfp-floatShadeHidden{opacity:0;transition:none}",
|
|
5826
|
+
".dfp-floatShade.dfp-floatCardLeave{opacity:0;transition:opacity .46s cubic-bezier(.45,0,.25,1)}",
|
|
5654
5827
|
// the arming frame, on for the two frames the enter class is on: measuring the
|
|
5655
5828
|
// card for `--dfp-clipStart` RESOLVES its style, and without this the resting
|
|
5656
5829
|
// clip resolved there would be the "before" of a transition the moment the
|
|
@@ -5667,30 +5840,42 @@ var CARD_CSS = [
|
|
|
5667
5840
|
// so the title never runs underneath it; there is no slot for the dot any
|
|
5668
5841
|
// more — the dot is faded out while the card is open.
|
|
5669
5842
|
".dfp-floatHead{display:flex;align-items:center;gap:8px;cursor:grab;user-select:none;touch-action:none}",
|
|
5670
|
-
".dfp-floatTitle{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:
|
|
5843
|
+
".dfp-floatTitle{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:24px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
5671
5844
|
// the close: a round icon button in the quick-toc style — same tint at rest,
|
|
5672
5845
|
// the next tint step on hover; the rounded bar inside is the close icon
|
|
5673
|
-
".dfp-floatClose{position:absolute;top:
|
|
5846
|
+
".dfp-floatClose{position:absolute;top:11px;right:8px;z-index:2;appearance:none;cursor:pointer;display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:none;border-radius:50%;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.16));transition:background .15s ease,color .15s ease,opacity .2s ease}",
|
|
5674
5847
|
".dfp-floatClose:hover{background:var(--dsw-alias-interactive-bg-active,rgba(79,140,255,.24))}",
|
|
5675
5848
|
".dfp-floatCloseCross{display:block;pointer-events:none;color:currentColor}",
|
|
5676
5849
|
// the settings entry: the VERY same circle as the close (same diameter, same
|
|
5677
5850
|
// tint, same hover step), pinned to the card's corner and pushed one diameter
|
|
5678
5851
|
// plus FLOAT_SETTINGS_GAP to its left. Both radii are 50% and the boxes are
|
|
5679
5852
|
// square, so neither can read as an ellipse.
|
|
5680
|
-
".dfp-floatSettings{position:absolute;top:
|
|
5853
|
+
".dfp-floatSettings{position:absolute;top:11px;right:38px;z-index:2;appearance:none;cursor:pointer;display:flex;align-items:center;justify-content:center;width:22px;height:22px;min-width:0;padding:0;border:none;border-radius:50%;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.16));transition:background .15s ease,color .15s ease,opacity .2s ease}",
|
|
5681
5854
|
".dfp-floatSettings:hover{background:var(--dsw-alias-interactive-bg-active,rgba(79,140,255,.24))}",
|
|
5682
5855
|
".dfp-floatSettingsGlyph{display:block;pointer-events:none;color:currentColor}",
|
|
5683
5856
|
// the host's own button reset cannot inflate either circle: the two share one
|
|
5684
5857
|
// box model, so "the same size" is a property of the pair, not of a wish.
|
|
5685
5858
|
".dfp-floatClose,.dfp-floatSettings{box-sizing:border-box;aspect-ratio:1/1;flex:0 0 auto}",
|
|
5686
5859
|
".dfp-floatResize{position:absolute;z-index:3;touch-action:none}",
|
|
5860
|
+
// Width only. The bottom and corner grips let a press land on the panel's own
|
|
5861
|
+
// last row instead of on the control there, which reads as a dead slider; the
|
|
5862
|
+
// height is content-driven anyway (the card caps itself at the viewport), so
|
|
5863
|
+
// the two vertical grips cost more than they gave.
|
|
5687
5864
|
".dfp-floatResizeR{top:0;right:0;width:8px;height:100%;cursor:ew-resize}",
|
|
5688
|
-
".dfp-floatResizeB{left:0;bottom:0;width:100%;height:8px;cursor:ns-resize}",
|
|
5689
|
-
".dfp-floatResizeBR{right:0;bottom:0;width:14px;height:14px;cursor:nwse-resize}",
|
|
5690
5865
|
".dfp-floatRow{margin-top:8px}",
|
|
5691
5866
|
".dfp-floatLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;display:flex;justify-content:space-between;gap:8px}",
|
|
5692
5867
|
".dfp-floatValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}",
|
|
5693
|
-
|
|
5868
|
+
// `margin:0` is load-bearing: Chromium's UA sheet gives `input[type=range]` a
|
|
5869
|
+
// 2px margin on every side, which sits OUTSIDE the `width:100%` box. The row is
|
|
5870
|
+
// then 4px wider than the scrolling body, whose `overflow-x:hidden` shaves the
|
|
5871
|
+
// slider's right end (measured: scrollWidth 288 vs clientWidth 284, slider
|
|
5872
|
+
// ending 2px past the body). `display:block` keeps it from adding a line box.
|
|
5873
|
+
".dfp-floatSlider{display:block;margin:0;width:100%;box-sizing:border-box;accent-color:var(--dsw-alias-brand-primary)}",
|
|
5874
|
+
// The preset picker shares the head row: the title takes the slack, so this
|
|
5875
|
+
// sits against the right edge of the head's own content box — i.e. just left of
|
|
5876
|
+
// the two round buttons the head reserves space for.
|
|
5877
|
+
".dfp-floatPreset{flex:none;max-width:118px;font:inherit;font-size:12px;line-height:18px;padding:2px 4px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l3);border-radius:8px;cursor:pointer}",
|
|
5878
|
+
".dfp-floatPreset:disabled{opacity:.5;cursor:default}",
|
|
5694
5879
|
// the write-failure line takes no room at all while it carries no message
|
|
5695
5880
|
".dfp-floatStatus{margin-top:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
5696
5881
|
".dfp-floatStatus:empty{display:none}",
|
|
@@ -8150,12 +8335,21 @@ function FontCard(props) {
|
|
|
8150
8335
|
/** The pending auto-hide / fade-out timers of the preset message. */
|
|
8151
8336
|
var statusIdRef = useRef(0);
|
|
8152
8337
|
var statusTimerRef = useRef(null);
|
|
8338
|
+
/**
|
|
8339
|
+
* Which surface the controls edit when the two themes are set separately.
|
|
8340
|
+
*
|
|
8341
|
+
* Off means the flat fields (the light set), on means the dark overrides. This
|
|
8342
|
+
* is view state only — it never reaches the document — and it is ignored while
|
|
8343
|
+
* `perTheme` is off, because the two sets are then the same object.
|
|
8344
|
+
*/
|
|
8345
|
+
var [editingDark, setEditingDark] = useState(false);
|
|
8153
8346
|
var snapshot = useScopeSnapshot(scope);
|
|
8154
8347
|
var config = normalizeConfig(snapshot.value);
|
|
8155
|
-
//
|
|
8156
|
-
//
|
|
8157
|
-
|
|
8348
|
+
// Per-theme is honoured exactly as stored (0.7.0). It used to be forced off
|
|
8349
|
+
// here because nothing in the UI could edit the dark set; `editingDark` is
|
|
8350
|
+
// the control that made forcing it unnecessary.
|
|
8158
8351
|
var sets = resolveAxes(config);
|
|
8352
|
+
var themesSplit = config[PER_THEME_FIELD] === true;
|
|
8159
8353
|
var user = snapshot.user !== null && typeof snapshot.user === "object" ? snapshot.user : {};
|
|
8160
8354
|
var writable = snapshot.writable === true;
|
|
8161
8355
|
var presets = config[PRESETS_FIELD];
|
|
@@ -8176,9 +8370,9 @@ function FontCard(props) {
|
|
|
8176
8370
|
}
|
|
8177
8371
|
if (!activeKnown) activeName = presetList[0].name;
|
|
8178
8372
|
|
|
8179
|
-
// Which value set the controls below edit and display
|
|
8180
|
-
//
|
|
8181
|
-
var editing = sets.light;
|
|
8373
|
+
// Which value set the controls below edit and display. With the split off the
|
|
8374
|
+
// two sets are the same object, so the condition costs nothing.
|
|
8375
|
+
var editing = themesSplit && editingDark ? sets.dark : sets.light;
|
|
8182
8376
|
|
|
8183
8377
|
var changeView = function (next) {
|
|
8184
8378
|
setView(next);
|
|
@@ -8204,6 +8398,11 @@ function FontCard(props) {
|
|
|
8204
8398
|
// Chrome (the floating panel's switch and position) never rides a preset:
|
|
8205
8399
|
// switching presets must not hide or move the panel.
|
|
8206
8400
|
if (VALUE_FIELDS.indexOf(field) < 0) return;
|
|
8401
|
+
// A dark edit must not ride the preset either. The snapshot stores the LIGHT
|
|
8402
|
+
// set, so mirroring a dark value into a flat field would apply that value to
|
|
8403
|
+
// the light set the next time this preset is picked — the preset would slowly
|
|
8404
|
+
// turn into a copy of the dark overrides.
|
|
8405
|
+
if (themesSplit && editingDark) return;
|
|
8207
8406
|
if (!writable || applyingRef.current === true) {
|
|
8208
8407
|
probeLog("mirror-skipped", { field: field, writable: writable, applying: applyingRef.current });
|
|
8209
8408
|
return;
|
|
@@ -8570,10 +8769,60 @@ function FontCard(props) {
|
|
|
8570
8769
|
pendingOps[field] = { op: op, value: value };
|
|
8571
8770
|
scheduleFlush();
|
|
8572
8771
|
};
|
|
8772
|
+
/**
|
|
8773
|
+
* Whether a write for this field belongs to the dark override layer.
|
|
8774
|
+
*
|
|
8775
|
+
* Only value axes can be overridden per theme: the theme flag itself, the
|
|
8776
|
+
* preset storage, the weight scale marker and the panel chrome are not part
|
|
8777
|
+
* of either set, so each of them writes its own field whatever surface is
|
|
8778
|
+
* being edited.
|
|
8779
|
+
* @param {string} field - a settings field name.
|
|
8780
|
+
* @returns {boolean} true when the write must go into `darkValues`.
|
|
8781
|
+
*/
|
|
8782
|
+
var writesDark = function (field) {
|
|
8783
|
+
if (!themesSplit || !editingDark) return false;
|
|
8784
|
+
return VALUE_FIELDS.indexOf(field) >= 0;
|
|
8785
|
+
};
|
|
8786
|
+
/**
|
|
8787
|
+
* The dark map a write should merge into, queued writes included.
|
|
8788
|
+
*
|
|
8789
|
+
* Reading the document alone would lose a value the user moved one frame ago:
|
|
8790
|
+
* the batch has not been flushed yet, so the document still holds the older
|
|
8791
|
+
* map and the second click would overwrite the first one's override.
|
|
8792
|
+
* @returns {Record<string, string|number|boolean>} the sparse map to merge into.
|
|
8793
|
+
*/
|
|
8794
|
+
var pendingDarkMap = function () {
|
|
8795
|
+
var queued = pendingOps[DARK_VALUES_FIELD];
|
|
8796
|
+
return shared.normalizeDarkValues(queued !== undefined ? queued.value : config[DARK_VALUES_FIELD]);
|
|
8797
|
+
};
|
|
8798
|
+
/**
|
|
8799
|
+
* Write one axis into the dark override layer instead of the flat fields.
|
|
8800
|
+
*
|
|
8801
|
+
* The document stores the whole map as one JSON string, so one axis edit is
|
|
8802
|
+
* one whole-map write; `undefined` removes the override and hands the field
|
|
8803
|
+
* back to the light set.
|
|
8804
|
+
* @param {string} field - a value-axis field name.
|
|
8805
|
+
* @param {string|number|boolean|undefined} value - the override, or undefined to clear it.
|
|
8806
|
+
*/
|
|
8807
|
+
var submitDark = function (field, value) {
|
|
8808
|
+
var next = shared.withDarkOverride(pendingDarkMap(), field, value);
|
|
8809
|
+
// A dark override in a document that has the split switched off would render
|
|
8810
|
+
// nothing at all, so the flag rides the same batch.
|
|
8811
|
+
if (config[PER_THEME_FIELD] !== true) queuePatch(PER_THEME_FIELD, shared.PENDING_SET, true);
|
|
8812
|
+
queuePatch(DARK_VALUES_FIELD, shared.PENDING_SET, shared.darkValuesText(next));
|
|
8813
|
+
};
|
|
8573
8814
|
var submit = function (field, value) {
|
|
8815
|
+
if (writesDark(field)) {
|
|
8816
|
+
submitDark(field, value);
|
|
8817
|
+
return;
|
|
8818
|
+
}
|
|
8574
8819
|
queuePatch(field, shared.PENDING_SET, value);
|
|
8575
8820
|
};
|
|
8576
8821
|
var clear = function (field) {
|
|
8822
|
+
if (writesDark(field)) {
|
|
8823
|
+
submitDark(field, undefined);
|
|
8824
|
+
return;
|
|
8825
|
+
}
|
|
8577
8826
|
queuePatch(field, shared.PENDING_UNSET, undefined);
|
|
8578
8827
|
};
|
|
8579
8828
|
/** The frame handle for the next local paint; null when no paint is scheduled. */
|
|
@@ -9629,6 +9878,62 @@ function FontCard(props) {
|
|
|
9629
9878
|
: null
|
|
9630
9879
|
),
|
|
9631
9880
|
|
|
9881
|
+
// Per-theme. The sync switch follows every other on/off row in this
|
|
9882
|
+
// card (label left, joined segmented pushed right); the surface picker
|
|
9883
|
+
// is a second row under the description, and it fades in and out with
|
|
9884
|
+
// the switch — both rows are always mounted so nothing else moves.
|
|
9885
|
+
h(
|
|
9886
|
+
FieldShell,
|
|
9887
|
+
{
|
|
9888
|
+
t: t,
|
|
9889
|
+
label: t("theme.label"),
|
|
9890
|
+
hint: t("theme.sharedHint"),
|
|
9891
|
+
disabled: !writable,
|
|
9892
|
+
inline: true,
|
|
9893
|
+
},
|
|
9894
|
+
h(Segmented, {
|
|
9895
|
+
label: t("theme.label"),
|
|
9896
|
+
// "On" is the shared set: that is the default, and it is what the
|
|
9897
|
+
// switch means to a reader — syncing the two themes.
|
|
9898
|
+
value: themesSplit ? 0 : 1,
|
|
9899
|
+
disabled: !writable,
|
|
9900
|
+
options: [
|
|
9901
|
+
{ value: 0, label: t("shape.off") },
|
|
9902
|
+
{ value: 1, label: t("shape.on") },
|
|
9903
|
+
],
|
|
9904
|
+
onChange: function (next) {
|
|
9905
|
+
var synced = next === 1;
|
|
9906
|
+
submit(PER_THEME_FIELD, !synced);
|
|
9907
|
+
if (synced) setEditingDark(false);
|
|
9908
|
+
},
|
|
9909
|
+
})
|
|
9910
|
+
),
|
|
9911
|
+
h(
|
|
9912
|
+
"div",
|
|
9913
|
+
{ className: "dfp-themeSplit" + (themesSplit ? " dfp-themeSplitOn" : "") },
|
|
9914
|
+
h(
|
|
9915
|
+
"div",
|
|
9916
|
+
{ className: "dfp-themeSplitClip" },
|
|
9917
|
+
h(
|
|
9918
|
+
"div",
|
|
9919
|
+
{ className: "dfp-themeSplitRow" },
|
|
9920
|
+
h(Segmented, {
|
|
9921
|
+
label: t("theme.surface"),
|
|
9922
|
+
value: editingDark ? 1 : 0,
|
|
9923
|
+
disabled: !writable,
|
|
9924
|
+
options: [
|
|
9925
|
+
{ value: 0, label: t("theme.light") },
|
|
9926
|
+
{ value: 1, label: t("theme.dark") },
|
|
9927
|
+
],
|
|
9928
|
+
onChange: function (next) {
|
|
9929
|
+
setEditingDark(next === 1);
|
|
9930
|
+
},
|
|
9931
|
+
}),
|
|
9932
|
+
h("span", { className: "dfp-themeNote" }, t("theme.splitHint"))
|
|
9933
|
+
)
|
|
9934
|
+
)
|
|
9935
|
+
),
|
|
9936
|
+
|
|
9632
9937
|
// the accordion, conversation first (it owns every axis), then the
|
|
9633
9938
|
// interface (which follows it), then code
|
|
9634
9939
|
h(
|
|
@@ -9966,7 +10271,7 @@ var FLOAT_OPEN_KEY = "dsh-fonttune.float.v1";
|
|
|
9966
10271
|
*
|
|
9967
10272
|
* Used to know when the card's resting clip may be dropped for good.
|
|
9968
10273
|
*/
|
|
9969
|
-
var FLOAT_UNFOLD_MS =
|
|
10274
|
+
var FLOAT_UNFOLD_MS = 320;
|
|
9970
10275
|
|
|
9971
10276
|
/**
|
|
9972
10277
|
* The fold's length, in milliseconds (mirrors `.dfp-floatCardLeave`).
|
|
@@ -9986,7 +10291,7 @@ var FLOAT_FOLD_MS = 420;
|
|
|
9986
10291
|
* for the two frames the class waits for (see `startLeave`) so the card is
|
|
9987
10292
|
* never dropped mid-fold.
|
|
9988
10293
|
*/
|
|
9989
|
-
var FLOAT_LEAVE_MS =
|
|
10294
|
+
var FLOAT_LEAVE_MS = 460;
|
|
9990
10295
|
|
|
9991
10296
|
/**
|
|
9992
10297
|
* How long the enter frame may wait for its two frames before a timer stands
|
|
@@ -10095,7 +10400,32 @@ function floatStoreOpen(open) {
|
|
|
10095
10400
|
* the settings button performs at click time.
|
|
10096
10401
|
* @returns {{destroy: () => void, toggle: () => void}} the panel's handles.
|
|
10097
10402
|
*/
|
|
10403
|
+
/**
|
|
10404
|
+
* Whether this browser can blur what is behind an element.
|
|
10405
|
+
*
|
|
10406
|
+
* Asked once, at load: the answer cannot change while the page lives, and the
|
|
10407
|
+
* fallback (the same surface, opaque) is what the sheet already draws when the
|
|
10408
|
+
* flag class is absent.
|
|
10409
|
+
* @returns {boolean} true when `backdrop-filter` is usable.
|
|
10410
|
+
*/
|
|
10411
|
+
var GLASS_OK = (function () {
|
|
10412
|
+
try {
|
|
10413
|
+
return (
|
|
10414
|
+
typeof CSS !== "undefined" &&
|
|
10415
|
+
typeof CSS.supports === "function" &&
|
|
10416
|
+
(CSS.supports("backdrop-filter", "blur(12px)") ||
|
|
10417
|
+
CSS.supports("-webkit-backdrop-filter", "blur(12px)"))
|
|
10418
|
+
);
|
|
10419
|
+
} catch (error) {
|
|
10420
|
+
return false;
|
|
10421
|
+
}
|
|
10422
|
+
})();
|
|
10423
|
+
|
|
10098
10424
|
function mountFloatPanel(scope, t, hostCtx) {
|
|
10425
|
+
/** How far the panel's drop shadow reaches past its own box (10px offset + blur). */
|
|
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;
|
|
10099
10429
|
var noop = function () {};
|
|
10100
10430
|
if (typeof document === "undefined" || !document.body) return { destroy: noop, toggle: noop };
|
|
10101
10431
|
// The offline stand-ins hand out element stubs without listeners or
|
|
@@ -10166,8 +10496,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10166
10496
|
// The document value `pos` was last reconciled with: a snapshot carrying
|
|
10167
10497
|
// anything else is an edit from elsewhere and is adopted. Our own writes
|
|
10168
10498
|
// set `pos` first, so their echo always matches and is a no-op.
|
|
10169
|
-
|
|
10170
|
-
|
|
10499
|
+
/** Watches the theme attribute so the panel re-renders on a theme switch. */
|
|
10500
|
+
var themeWatcher = null;
|
|
10501
|
+
|
|
10502
|
+
|
|
10503
|
+
var lastSeenDoc = null; // The same for the corner: the document value `corner` was last reconciled
|
|
10171
10504
|
// with, and the one our own write is still carrying.
|
|
10172
10505
|
var lastSeenCorner = null;
|
|
10173
10506
|
var lastSeenSize = null;
|
|
@@ -10184,6 +10517,19 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10184
10517
|
// end on the same small rectangle the unfold started from, and the card's own
|
|
10185
10518
|
// measurement only exists while it is mounted.
|
|
10186
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;
|
|
10187
10533
|
|
|
10188
10534
|
var el = function (tag, className, text) {
|
|
10189
10535
|
var node = document.createElement(tag);
|
|
@@ -10562,6 +10908,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10562
10908
|
return null;
|
|
10563
10909
|
};
|
|
10564
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
|
+
|
|
10565
10916
|
/** The window size the bounds are cut to, with the shared fallback. */
|
|
10566
10917
|
var floatViewport = function () {
|
|
10567
10918
|
return {
|
|
@@ -10691,10 +11042,19 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10691
11042
|
* The conversation when it can be found, the whole window when it cannot (a
|
|
10692
11043
|
* settings page has no prose at all): the window is what this used to be
|
|
10693
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".
|
|
10694
11052
|
* @returns {object} the bounds, in viewport coordinates.
|
|
10695
11053
|
*/
|
|
10696
11054
|
var floatBounds = function () {
|
|
10697
|
-
|
|
11055
|
+
var picked = shared.floatBoundsPick(floatBoundsRects(), floatViewport());
|
|
11056
|
+
boundsAreReal = picked.index >= 0;
|
|
11057
|
+
return picked.bounds;
|
|
10698
11058
|
};
|
|
10699
11059
|
|
|
10700
11060
|
/**
|
|
@@ -10735,16 +11095,206 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10735
11095
|
} catch (error) {
|
|
10736
11096
|
// An unmeasurable card keeps the flat (rest) clip; only the shape is lost.
|
|
10737
11097
|
}
|
|
11098
|
+
var start = shared.floatClipStart(width, height, quadrant, shared.FLOAT_DOT);
|
|
10738
11099
|
try {
|
|
10739
|
-
card.style.setProperty(
|
|
10740
|
-
"--dfp-clipStart",
|
|
10741
|
-
shared.floatClipStart(width, height, quadrant, shared.FLOAT_DOT)
|
|
10742
|
-
);
|
|
11100
|
+
card.style.setProperty("--dfp-clipStart", start);
|
|
10743
11101
|
} catch (error) {
|
|
10744
11102
|
// A card without a style face falls back to the flat clip in the sheet.
|
|
10745
11103
|
}
|
|
10746
11104
|
};
|
|
10747
11105
|
|
|
11106
|
+
/**
|
|
11107
|
+
* Copy the card's clip onto the glass layer, frame by frame.
|
|
11108
|
+
*
|
|
11109
|
+
* The glass is a sibling (a blur inside the card samples nothing — see the sheet),
|
|
11110
|
+
* so the card's clip does not reach it, and two CSS transitions on the same
|
|
11111
|
+
* property are NOT one animation: Chrome retargets them independently and the
|
|
11112
|
+
* glass came out at a quarter of the card's duration. Reading the card's computed
|
|
11113
|
+
* clip and writing it down is the only way to guarantee one shape at every frame.
|
|
11114
|
+
* @param {object} card - the live card element.
|
|
11115
|
+
* @param {object} glass - the blur layer beside it.
|
|
11116
|
+
* @param {number} ms - how long to keep copying (the animation's own length).
|
|
11117
|
+
*/
|
|
11118
|
+
/**
|
|
11119
|
+
* The same `inset()` shape, grown outwards by `grow` pixels on every side.
|
|
11120
|
+
*
|
|
11121
|
+
* Negative insets are legal and paint beyond the border box, which is the only
|
|
11122
|
+
* way an element can follow a silhouette while its own shadow — which lies
|
|
11123
|
+
* outside that box — still survives the clip.
|
|
11124
|
+
* @param {string} clip - a computed inset() clip.
|
|
11125
|
+
* @param {number} grow - how far to push every edge outwards.
|
|
11126
|
+
* @returns {string} the grown clip, or the input when it is not an inset().
|
|
11127
|
+
*/
|
|
11128
|
+
var growClip = function (clip, grow) {
|
|
11129
|
+
var found = /^inset\((-?[\d.]+)px (-?[\d.]+)px (-?[\d.]+)px (-?[\d.]+)px( round .*)?\)$/.exec(clip);
|
|
11130
|
+
if (found === null) return clip;
|
|
11131
|
+
return (
|
|
11132
|
+
"inset(" +
|
|
11133
|
+
(Number(found[1]) - grow) +
|
|
11134
|
+
"px " +
|
|
11135
|
+
(Number(found[2]) - grow) +
|
|
11136
|
+
"px " +
|
|
11137
|
+
(Number(found[3]) - grow) +
|
|
11138
|
+
"px " +
|
|
11139
|
+
(Number(found[4]) - grow) +
|
|
11140
|
+
"px" +
|
|
11141
|
+
(found[5] || "") +
|
|
11142
|
+
")"
|
|
11143
|
+
);
|
|
11144
|
+
};
|
|
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
|
+
};
|
|
11176
|
+
var mirrorGlassClip = function (card, glass, ms) { if (glass === null || glass === undefined) return;
|
|
11177
|
+
var shade = host.querySelector(".dfp-floatShade");
|
|
11178
|
+
var until = Date.now() + ms;
|
|
11179
|
+
var step = function () {
|
|
11180
|
+
if (card.parentNode === null || glass.parentNode === null) return;
|
|
11181
|
+
var clip = "none";
|
|
11182
|
+
try {
|
|
11183
|
+
clip = globalThis.getComputedStyle(card).clipPath;
|
|
11184
|
+
} catch (error) {
|
|
11185
|
+
return;
|
|
11186
|
+
}
|
|
11187
|
+
glass.style.clipPath = clip;
|
|
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);
|
|
11193
|
+
if (Date.now() < until) globalThis.requestAnimationFrame(step);
|
|
11194
|
+
};
|
|
11195
|
+
if (typeof globalThis.requestAnimationFrame === "function") globalThis.requestAnimationFrame(step);
|
|
11196
|
+
};
|
|
11197
|
+
|
|
11198
|
+
/**
|
|
11199
|
+
* Give the shade layer the card's unfold class.
|
|
11200
|
+
*
|
|
11201
|
+
* The shade is the sibling that carries the panel's drop shadow, and it has to
|
|
11202
|
+
* leave and arrive with the card: without this it sat at its full resting size
|
|
11203
|
+
* through the whole unfold and the whole fold and was then dropped with the
|
|
11204
|
+
* element, which is what a bare frame hanging around the conversation looked
|
|
11205
|
+
* like. It takes the class for its OPACITY rules only — it deliberately does not
|
|
11206
|
+
* mirror the clip (see the note on `.dfp-floatShade`).
|
|
11207
|
+
* @param {string} name - "dfp-floatCardEnter" or "dfp-floatCardLeave".
|
|
11208
|
+
* @param {boolean} on - true to add, false to remove.
|
|
11209
|
+
*/
|
|
11210
|
+
var setShadeClass = function (name, on) {
|
|
11211
|
+
var shade = host.querySelector(".dfp-floatShade");
|
|
11212
|
+
if (shade === null || shade === undefined || typeof shade.classList === "undefined") return;
|
|
11213
|
+
if (on) shade.classList.add(name);
|
|
11214
|
+
else shade.classList.remove(name);
|
|
11215
|
+
};
|
|
11216
|
+
|
|
11217
|
+
/**
|
|
11218
|
+
* Whether the page is showing the dark theme right now.
|
|
11219
|
+
*
|
|
11220
|
+
* Read from the attribute DSH itself switches (`body[data-ds-dark-theme]`),
|
|
11221
|
+
* never from a media query: the reader can pick a theme that disagrees with the
|
|
11222
|
+
* system preference, and the rules this panel edits are the ones that attribute
|
|
11223
|
+
* prefixes.
|
|
11224
|
+
* @returns {boolean} true while the dark theme is on screen.
|
|
11225
|
+
*/
|
|
11226
|
+
var pageIsDark = function () {
|
|
11227
|
+
try {
|
|
11228
|
+
return document.body.hasAttribute("data-ds-dark-theme");
|
|
11229
|
+
} catch (error) {
|
|
11230
|
+
return false;
|
|
11231
|
+
}
|
|
11232
|
+
};
|
|
11233
|
+
|
|
11234
|
+
/**
|
|
11235
|
+
* The configuration the panel edits: the themed view when the split is on.
|
|
11236
|
+
*
|
|
11237
|
+
* With `perTheme` on, the panel edits whichever set the page is showing — the
|
|
11238
|
+
* reader asked for "light only edits light, dark only edits dark". The view is
|
|
11239
|
+
* the flat configuration with every value axis replaced by the resolved set, so
|
|
11240
|
+
* every `config[field]` read in this file follows the theme without knowing
|
|
11241
|
+
* about it; storage fields (presets, the dark map itself) are untouched.
|
|
11242
|
+
* @param {Record<string, unknown>} config - a normalized configuration.
|
|
11243
|
+
* @returns {Record<string, unknown>} the view to edit and display.
|
|
11244
|
+
*/
|
|
11245
|
+
var themeView = function (config) {
|
|
11246
|
+
if (config[PER_THEME_FIELD] !== true || !pageIsDark()) return config;
|
|
11247
|
+
var dark = resolveAxes(config).dark;
|
|
11248
|
+
var view = {};
|
|
11249
|
+
var key;
|
|
11250
|
+
for (key in config) {
|
|
11251
|
+
if (Object.prototype.hasOwnProperty.call(config, key)) view[key] = config[key];
|
|
11252
|
+
}
|
|
11253
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
11254
|
+
var field = VALUE_FIELDS[index];
|
|
11255
|
+
if (dark[field] !== undefined) view[field] = dark[field];
|
|
11256
|
+
}
|
|
11257
|
+
return view;
|
|
11258
|
+
};
|
|
11259
|
+
|
|
11260
|
+
/**
|
|
11261
|
+
* Route one batch of panel changes to the set the page is showing.
|
|
11262
|
+
*
|
|
11263
|
+
* Light: the batch passes through untouched. Dark: the value axes are merged
|
|
11264
|
+
* into the sparse `darkValues` map and the batch becomes one whole-map write,
|
|
11265
|
+
* which is exactly what the card does — the two surfaces must not disagree
|
|
11266
|
+
* about where a slider writes.
|
|
11267
|
+
* @param {Record<string, unknown>} changes - field to value.
|
|
11268
|
+
* @param {Record<string, unknown>} config - the stored (not themed) configuration.
|
|
11269
|
+
* @returns {Record<string, unknown>} the batch to send.
|
|
11270
|
+
*/
|
|
11271
|
+
var routeForTheme = function (changes, config) {
|
|
11272
|
+
if (config[PER_THEME_FIELD] !== true || !pageIsDark()) return changes;
|
|
11273
|
+
var axes = {};
|
|
11274
|
+
var rest = {};
|
|
11275
|
+
var touched = false;
|
|
11276
|
+
var key;
|
|
11277
|
+
for (key in changes) {
|
|
11278
|
+
if (!Object.prototype.hasOwnProperty.call(changes, key)) continue;
|
|
11279
|
+
if (VALUE_FIELDS.indexOf(key) >= 0) {
|
|
11280
|
+
axes[key] = changes[key];
|
|
11281
|
+
touched = true;
|
|
11282
|
+
} else {
|
|
11283
|
+
rest[key] = changes[key];
|
|
11284
|
+
}
|
|
11285
|
+
}
|
|
11286
|
+
if (!touched) return changes;
|
|
11287
|
+
var map = config[DARK_VALUES_FIELD];
|
|
11288
|
+
for (key in axes) {
|
|
11289
|
+
if (Object.prototype.hasOwnProperty.call(axes, key)) {
|
|
11290
|
+
map = shared.withDarkOverride(map, key, axes[key]);
|
|
11291
|
+
}
|
|
11292
|
+
}
|
|
11293
|
+
rest[DARK_VALUES_FIELD] = shared.darkValuesText(map);
|
|
11294
|
+
if (config[PER_THEME_FIELD] !== true) rest[PER_THEME_FIELD] = true;
|
|
11295
|
+
return rest;
|
|
11296
|
+
};
|
|
11297
|
+
|
|
10748
11298
|
/**
|
|
10749
11299
|
* The state the panel shows: the document overlaid with the local moves,
|
|
10750
11300
|
* so the panel always agrees with the page.
|
|
@@ -10757,7 +11307,8 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10757
11307
|
// An unready scope reads as an empty, read-only document.
|
|
10758
11308
|
}
|
|
10759
11309
|
var raw = snap.value !== null && typeof snap.value === "object" ? snap.value : {};
|
|
10760
|
-
var
|
|
11310
|
+
var stored = normalizeConfig(shared.overlayPendingValues(raw, pendingLocalValues));
|
|
11311
|
+
var config = themeView(stored);
|
|
10761
11312
|
var stored = config[PRESETS_FIELD];
|
|
10762
11313
|
var list = Array.isArray(stored) && stored.length > 0
|
|
10763
11314
|
? stored
|
|
@@ -10985,6 +11536,56 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10985
11536
|
return fallback;
|
|
10986
11537
|
};
|
|
10987
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
|
+
|
|
10988
11589
|
/**
|
|
10989
11590
|
* Snap the dot to the corner it LIVES in, with a short slide.
|
|
10990
11591
|
*
|
|
@@ -11007,8 +11608,15 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11007
11608
|
* to send the dot to the top-left corner the user never asked for.
|
|
11008
11609
|
* @returns {{x: number, y: number}} the corner the dot now sits on.
|
|
11009
11610
|
*/
|
|
11010
|
-
var snapToCorner = function () {
|
|
11611
|
+
var snapToCorner = function (instant) {
|
|
11011
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;
|
|
11012
11620
|
var snapped;
|
|
11013
11621
|
if (pos !== null) {
|
|
11014
11622
|
snapped = cornerHome(bounds, pos);
|
|
@@ -11030,7 +11638,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11030
11638
|
}
|
|
11031
11639
|
pos = snapped;
|
|
11032
11640
|
try {
|
|
11033
|
-
|
|
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");
|
|
11034
11646
|
} catch (error) {
|
|
11035
11647
|
// A host without classes still lands on the snapped corner.
|
|
11036
11648
|
}
|
|
@@ -11141,9 +11753,15 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11141
11753
|
* dot rather than the document handing it a position.
|
|
11142
11754
|
* @returns {boolean} true when the dot was moved onto its corner.
|
|
11143
11755
|
*/
|
|
11144
|
-
var settleOnCorner = function (regionMoved) {
|
|
11756
|
+
var settleOnCorner = function (regionMoved, instant) {
|
|
11145
11757
|
if (drag !== null || resize !== null) return false;
|
|
11146
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;
|
|
11147
11765
|
var at = pos;
|
|
11148
11766
|
if (at === null) {
|
|
11149
11767
|
var rect = rectOf(host);
|
|
@@ -11183,7 +11801,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11183
11801
|
at.y + shared.FLOAT_DOT <= bounds.bottom + 0.5
|
|
11184
11802
|
);
|
|
11185
11803
|
pos = { x: Math.round(at.x), y: Math.round(at.y) };
|
|
11186
|
-
snapToCorner();
|
|
11804
|
+
snapToCorner(instant);
|
|
11187
11805
|
// Both fields in ONE write: the dot is on the corner and the document is
|
|
11188
11806
|
// told which corner that is, in the same `mutate`. A region that changed
|
|
11189
11807
|
// shape is the caller that has to store the move it just made, wherever the
|
|
@@ -11208,8 +11826,22 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11208
11826
|
* `settleOnCorner`).
|
|
11209
11827
|
*/
|
|
11210
11828
|
var reflowIntoBounds = function () {
|
|
11211
|
-
|
|
11212
|
-
|
|
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
|
+
}
|
|
11213
11845
|
};
|
|
11214
11846
|
|
|
11215
11847
|
/**
|
|
@@ -11420,6 +12052,11 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11420
12052
|
// again here rather than trusted from the render that built the card.
|
|
11421
12053
|
armClipStart(card);
|
|
11422
12054
|
card.classList.add("dfp-floatCardLeave");
|
|
12055
|
+
setShadeClass("dfp-floatCardLeave", true);
|
|
12056
|
+
// The glass folds with it: same clip on both, frame by frame (see
|
|
12057
|
+
// `mirrorGlassClip`). Looked up here rather than captured, because this runs
|
|
12058
|
+
// long after the render that built the card.
|
|
12059
|
+
mirrorGlassClip(card, host.querySelector(".dfp-floatGlass"), FLOAT_LEAVE_MS + 80);
|
|
11423
12060
|
// The dot comes back on the same two frames and over the same span as the
|
|
11424
12061
|
// fold, so the two cross-fades end together. It is a live element (the
|
|
11425
12062
|
// render that opened the panel built it), never a rebuilt one.
|
|
@@ -11498,7 +12135,10 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11498
12135
|
// browser cancelled) would otherwise unfold the card in the middle of the
|
|
11499
12136
|
// conversation. An ordinary open — the dot already on its corner — moves
|
|
11500
12137
|
// nothing at all.
|
|
11501
|
-
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();
|
|
11502
12142
|
playEnter = true;
|
|
11503
12143
|
if (closeTimer !== null) {
|
|
11504
12144
|
globalThis.clearTimeout(closeTimer);
|
|
@@ -11509,13 +12149,46 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11509
12149
|
render();
|
|
11510
12150
|
};
|
|
11511
12151
|
|
|
12152
|
+
/**
|
|
12153
|
+
* Apply one preset from the panel.
|
|
12154
|
+
*
|
|
12155
|
+
* Every value axis the entry stores, or that axis's neutral default when it
|
|
12156
|
+
* does not — the card's `switchPreset` contract, so the two surfaces agree on
|
|
12157
|
+
* what a preset means. It is ONE batch: a switch that landed field by field
|
|
12158
|
+
* would repaint the page through every intermediate state. `mirror: false`
|
|
12159
|
+
* because picking a preset is a selection, not an edit of that preset.
|
|
12160
|
+
* @param {{name: string, values: object}} entry - the picked preset.
|
|
12161
|
+
* @param {{presetList: object[], writable: boolean}} state - the panel's read.
|
|
12162
|
+
*/
|
|
12163
|
+
var applyPreset = function (entry, state) {
|
|
12164
|
+
if (!state.writable) return;
|
|
12165
|
+
var changes = {};
|
|
12166
|
+
for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
|
|
12167
|
+
var field = VALUE_FIELDS[index];
|
|
12168
|
+
changes[field] = Object.prototype.hasOwnProperty.call(entry.values, field)
|
|
12169
|
+
? entry.values[field]
|
|
12170
|
+
: DEFAULTS[field];
|
|
12171
|
+
}
|
|
12172
|
+
changes[ACTIVE_PRESET_FIELD] = entry.name;
|
|
12173
|
+
writeSingleShot(scope, state.presetList, entry.name, changes, {
|
|
12174
|
+
mirror: false,
|
|
12175
|
+
onError: reportError,
|
|
12176
|
+
});
|
|
12177
|
+
render();
|
|
12178
|
+
};
|
|
12179
|
+
|
|
11512
12180
|
var commit = function (changes, options) {
|
|
11513
12181
|
var state = readState();
|
|
11514
12182
|
if (!state.writable) return;
|
|
11515
12183
|
var settings = options !== null && typeof options === "object" ? options : {};
|
|
11516
12184
|
if (settings.mirror === undefined) settings.mirror = true;
|
|
12185
|
+
var routed = routeForTheme(changes, state.config);
|
|
12186
|
+
// A dark batch never rides a preset: the snapshot is the light set (the card's
|
|
12187
|
+
// mirror rule says the same), so mirroring it would store dark values as light
|
|
12188
|
+
// ones and the preset would drift.
|
|
12189
|
+
if (routed !== changes) settings.mirror = false;
|
|
11517
12190
|
settings.onError = reportError;
|
|
11518
|
-
writeSingleShot(scope, state.presetList, state.activeName,
|
|
12191
|
+
writeSingleShot(scope, state.presetList, state.activeName, routed, settings);
|
|
11519
12192
|
render();
|
|
11520
12193
|
};
|
|
11521
12194
|
|
|
@@ -11759,10 +12432,35 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11759
12432
|
// region is where it lives from now on. Every other path reads the
|
|
11760
12433
|
// remembered corner instead (see `settleOnCorner`), which is what keeps a
|
|
11761
12434
|
// region change from re-deciding it from stale coordinates.
|
|
11762
|
-
|
|
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.
|
|
11763
12459
|
snapToCorner();
|
|
11764
12460
|
var live = readState();
|
|
11765
12461
|
writeLivePos(live);
|
|
12462
|
+
// The rebuild that follows rides the snap: a side that changed glides.
|
|
12463
|
+
anchorGlide = open;
|
|
11766
12464
|
render();
|
|
11767
12465
|
};
|
|
11768
12466
|
var up = function () {
|
|
@@ -11842,19 +12540,14 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11842
12540
|
var move = function (at) {
|
|
11843
12541
|
if (resize === null) return;
|
|
11844
12542
|
resize.moved = true;
|
|
11845
|
-
var w =
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
}
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
}
|
|
11853
|
-
liveSize = { w: w, h: h };
|
|
11854
|
-
// The viewport cap rides the live size as well: what the pointer shows is
|
|
11855
|
-
// what the release stores, and the card can never leave the screen.
|
|
12543
|
+
var w = Math.min(
|
|
12544
|
+
shared.PANEL_SIZE_MAX_W,
|
|
12545
|
+
Math.max(shared.PANEL_SIZE_MIN_W, Math.round(resize.w + at.clientX - resize.startX))
|
|
12546
|
+
);
|
|
12547
|
+
liveSize = { w: w, h: 0 };
|
|
12548
|
+
// Width only: the height follows the content, so writing one here would pin
|
|
12549
|
+
// the panel to whatever it happened to measure at pointerdown.
|
|
11856
12550
|
box.style.width = "min(" + w + "px,calc(100vw - 32px))";
|
|
11857
|
-
box.style.height = "min(" + h + "px,calc(100vh - 24px))";
|
|
11858
12551
|
};
|
|
11859
12552
|
var end = function (write) {
|
|
11860
12553
|
globalThis.removeEventListener("pointermove", move);
|
|
@@ -11863,7 +12556,8 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11863
12556
|
var wasMoved = resize !== null && resize.moved;
|
|
11864
12557
|
resize = null;
|
|
11865
12558
|
if (write && wasMoved && liveSize !== null) {
|
|
11866
|
-
|
|
12559
|
+
// Height 0 means "no cap": the panel is as tall as its content.
|
|
12560
|
+
var cur = liveSize.w + ",0";
|
|
11867
12561
|
if (cur !== pendingSizeWrite) {
|
|
11868
12562
|
var live = readState();
|
|
11869
12563
|
if (live.writable) {
|
|
@@ -11978,12 +12672,23 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11978
12672
|
if (open && !(playEnter && !motionReduced()) && typeof dot.classList !== "undefined") {
|
|
11979
12673
|
dot.classList.add("dfp-dotAway");
|
|
11980
12674
|
}
|
|
11981
|
-
if (!open)
|
|
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
|
+
}
|
|
11982
12681
|
|
|
11983
12682
|
// The shadow's own layer: the card is clipped while it animates, and a clip
|
|
11984
12683
|
// on the card would clip the card's own filter away with it (see the sheet).
|
|
11985
12684
|
var shadow = el("div", "dfp-floatShadow");
|
|
11986
12685
|
var card = el("div", "dfp-floatCard");
|
|
12686
|
+
// The translucent surface behind the card. A SIBLING, never a child: a blur
|
|
12687
|
+
// inside the card would sample the card's own contents (the card fades to .6
|
|
12688
|
+
// and then 0 while it unfolds, and an element below opacity 1 is a backdrop
|
|
12689
|
+
// root). Mounted directly before the card at the end of this render.
|
|
12690
|
+
var glass = el("div", "dfp-floatGlass" + (GLASS_OK ? " dfp-floatGlassOn" : ""));
|
|
12691
|
+
// Mounted as the card's first child at the end of this render.
|
|
11987
12692
|
// The bar the dot sits in decides the expand direction, and that bar is the
|
|
11988
12693
|
// CONVERSATION's: `floatBounds` measures it, so a dot parked in the region's
|
|
11989
12694
|
// top-right grows left-down even where the window would call it bottom-right.
|
|
@@ -11998,15 +12703,24 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
11998
12703
|
// first one is a decision, not a coincidence.
|
|
11999
12704
|
anchor = { x: bounds.right, y: bounds.bottom };
|
|
12000
12705
|
}
|
|
12001
|
-
var quadrant
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
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
|
+
}
|
|
12010
12724
|
liveQuadrant = quadrant;
|
|
12011
12725
|
var expand = shared.floatExpandDirection(quadrant);
|
|
12012
12726
|
try {
|
|
@@ -12040,15 +12754,12 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12040
12754
|
// a card sized on a big screen must still fit a small one. The card fills
|
|
12041
12755
|
// the wrapper, so the wrapper is the box that gets the size.
|
|
12042
12756
|
shadow.style.width = "min(" + sized.w + "px,calc(100vw - 32px))";
|
|
12043
|
-
// The
|
|
12044
|
-
//
|
|
12045
|
-
//
|
|
12046
|
-
//
|
|
12047
|
-
//
|
|
12048
|
-
|
|
12049
|
-
// to size itself, except mid-drag, where the pointer's own height is in charge.
|
|
12050
|
-
card.style.maxHeight =
|
|
12051
|
-
sized.h > 0 ? "min(" + sized.h + "px,calc(100vh - 24px))" : "";
|
|
12757
|
+
// The height is the CONTENT's, always: the panel is exactly as tall as what
|
|
12758
|
+
// it holds, and the card's own `max-height: calc(100vh - 24px)` keeps a long
|
|
12759
|
+
// one inside the window. A stored height is no longer applied — the two
|
|
12760
|
+
// vertical grips that produced one are gone, and honouring an old value
|
|
12761
|
+
// would leave such a panel short with a scrollbar it cannot grow out of.
|
|
12762
|
+
card.style.maxHeight = "";
|
|
12052
12763
|
if (resize === null) shadow.style.height = "";
|
|
12053
12764
|
}
|
|
12054
12765
|
var head = el("div", "dfp-floatHead");
|
|
@@ -12067,6 +12778,32 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12067
12778
|
shared.FLOAT_CARD_PAD
|
|
12068
12779
|
) + "px";
|
|
12069
12780
|
head.appendChild(el("span", "dfp-floatTitle", t("panel.title")));
|
|
12781
|
+
// The preset picker lives in the head, right of the title: the panel is where
|
|
12782
|
+
// the page actually gets tuned, so switching a whole setup should not need a
|
|
12783
|
+
// trip to the settings sheet, and the head is the one row always on screen.
|
|
12784
|
+
if (state.presetList.length > 0) {
|
|
12785
|
+
var picker = el("select", "dfp-floatPreset");
|
|
12786
|
+
picker.setAttribute("aria-label", t("preset.label"));
|
|
12787
|
+
picker.title = t("preset.label");
|
|
12788
|
+
if (!state.writable) picker.disabled = true;
|
|
12789
|
+
for (var pickIndex = 0; pickIndex < state.presetList.length; pickIndex += 1) {
|
|
12790
|
+
var pickEntry = state.presetList[pickIndex];
|
|
12791
|
+
var pickOption = el("option", null, pickEntry.name);
|
|
12792
|
+
pickOption.value = pickEntry.name;
|
|
12793
|
+
if (pickEntry.name === state.activeName) pickOption.selected = true;
|
|
12794
|
+
picker.appendChild(pickOption);
|
|
12795
|
+
}
|
|
12796
|
+
picker.addEventListener("change", function () {
|
|
12797
|
+
var picked = readState();
|
|
12798
|
+
for (var scan = 0; scan < picked.presetList.length; scan += 1) {
|
|
12799
|
+
if (picked.presetList[scan].name === picker.value) {
|
|
12800
|
+
applyPreset(picked.presetList[scan], picked);
|
|
12801
|
+
return;
|
|
12802
|
+
}
|
|
12803
|
+
}
|
|
12804
|
+
});
|
|
12805
|
+
head.appendChild(picker);
|
|
12806
|
+
}
|
|
12070
12807
|
// The settings entry sits IMMEDIATELY LEFT of the close: the same circle, one
|
|
12071
12808
|
// diameter plus FLOAT_SETTINGS_GAP along the corner (see the stylesheet).
|
|
12072
12809
|
// Its own rule puts it there, so the pair stays 8px apart at every card size.
|
|
@@ -12102,8 +12839,13 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12102
12839
|
// this guard missing, the click the close button waits for never arrived.
|
|
12103
12840
|
head.addEventListener("pointerdown", function (event) {
|
|
12104
12841
|
var pressed = event.target;
|
|
12842
|
+
// Anything the reader can operate owns its own press: the two round buttons,
|
|
12843
|
+
// and the preset picker that now shares this row. A press that reached the
|
|
12844
|
+
// drag instead would preventDefault it, and a prevented pointerdown
|
|
12845
|
+
// suppresses the compatibility mouse events — the click the control waits
|
|
12846
|
+
// for would never arrive.
|
|
12105
12847
|
if (pressed !== null && pressed !== undefined && typeof pressed.closest === "function" &&
|
|
12106
|
-
pressed.closest("button") !== null) {
|
|
12848
|
+
pressed.closest("button,select,input,textarea,a") !== null) {
|
|
12107
12849
|
return;
|
|
12108
12850
|
}
|
|
12109
12851
|
startDrag(event, head, state.writable);
|
|
@@ -12298,27 +13040,56 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12298
13040
|
notches: shape.max - shape.min + 1,
|
|
12299
13041
|
});
|
|
12300
13042
|
card.appendChild(body);
|
|
12301
|
-
//
|
|
12302
|
-
//
|
|
13043
|
+
// One resize grip: the right edge, width only. It sits INSIDE the card, so a
|
|
13044
|
+
// press near the panel's right edge is still a resize and never a stray click
|
|
13045
|
+
// on whatever the row put there.
|
|
12303
13046
|
(function () {
|
|
12304
|
-
var
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
(function (edge) {
|
|
12311
|
-
var grip = el("div", edge.className);
|
|
12312
|
-
grip.setAttribute("data-dfp-resize", edge.mode);
|
|
12313
|
-
grip.addEventListener("pointerdown", function (event) {
|
|
12314
|
-
startResize(event, edge.mode, shadow, card);
|
|
12315
|
-
});
|
|
12316
|
-
card.appendChild(grip);
|
|
12317
|
-
})(edges[edgeIndex]);
|
|
12318
|
-
}
|
|
13047
|
+
var grip = el("div", "dfp-floatResize dfp-floatResizeR");
|
|
13048
|
+
grip.setAttribute("data-dfp-resize", "w");
|
|
13049
|
+
grip.addEventListener("pointerdown", function (event) {
|
|
13050
|
+
startResize(event, "w", shadow, card);
|
|
13051
|
+
});
|
|
13052
|
+
card.appendChild(grip);
|
|
12319
13053
|
})();
|
|
13054
|
+
var shadeEl = el("div", "dfp-floatShade");
|
|
13055
|
+
shadeEl.style.transformOrigin = cardCorner.origin;
|
|
13056
|
+
shadow.appendChild(shadeEl);
|
|
13057
|
+
shadow.appendChild(glass);
|
|
12320
13058
|
shadow.appendChild(card);
|
|
12321
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 };
|
|
12322
13093
|
// Enter animation, only on a real shut->open transition: the card starts
|
|
12323
13094
|
// clipped to the dot's own rectangle, a hair smaller and dimmer, and the
|
|
12324
13095
|
// next frames let the transition carry all three home (clip/transform/
|
|
@@ -12333,6 +13104,8 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12333
13104
|
card.classList.add("dfp-floatCardArm");
|
|
12334
13105
|
armClipStart(card);
|
|
12335
13106
|
card.classList.add("dfp-floatCardEnter");
|
|
13107
|
+
setShadeClass("dfp-floatShadeHidden", true);
|
|
13108
|
+
mirrorGlassClip(card, glass, FLOAT_UNFOLD_MS + 80);
|
|
12336
13109
|
var settled = false;
|
|
12337
13110
|
var settleTimer = null;
|
|
12338
13111
|
/**
|
|
@@ -12366,6 +13139,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12366
13139
|
return;
|
|
12367
13140
|
}
|
|
12368
13141
|
card.style.clipPath = "none";
|
|
13142
|
+
if (glass !== null && glass !== undefined) glass.style.clipPath = "none";
|
|
12369
13143
|
};
|
|
12370
13144
|
var settle = function () {
|
|
12371
13145
|
if (settled) return;
|
|
@@ -12377,6 +13151,9 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12377
13151
|
if (card.parentNode !== null) {
|
|
12378
13152
|
card.classList.remove("dfp-floatCardArm");
|
|
12379
13153
|
card.classList.remove("dfp-floatCardEnter");
|
|
13154
|
+
// Two frames in, with the pinned 0 already painted: releasing it now runs
|
|
13155
|
+
// the shade's fade over exactly the span the card spends growing.
|
|
13156
|
+
setShadeClass("dfp-floatShadeHidden", false);
|
|
12380
13157
|
}
|
|
12381
13158
|
// The dot goes out on the same two frames, so its own 1 paints first
|
|
12382
13159
|
// and the cross-fade runs with the unfold rather than before it.
|
|
@@ -12464,6 +13241,21 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12464
13241
|
} catch (error) {
|
|
12465
13242
|
// A host without an event target still watches through the observer.
|
|
12466
13243
|
}
|
|
13244
|
+
// The panel edits whichever set the page is showing, so a theme switch has to
|
|
13245
|
+
// rebuild it: without this, flipping to dark left every slider on the light
|
|
13246
|
+
// values, and the next drag wrote those back into the light set — the exact
|
|
13247
|
+
// opposite of what the reader asked for.
|
|
13248
|
+
try {
|
|
13249
|
+
themeWatcher = new MutationObserver(function () {
|
|
13250
|
+
render();
|
|
13251
|
+
});
|
|
13252
|
+
themeWatcher.observe(document.body, {
|
|
13253
|
+
attributes: true,
|
|
13254
|
+
attributeFilter: ["data-ds-dark-theme"],
|
|
13255
|
+
});
|
|
13256
|
+
} catch (error) {
|
|
13257
|
+
// A host without MutationObserver keeps the panel on the set it opened with.
|
|
13258
|
+
}
|
|
12467
13259
|
watchRegion();
|
|
12468
13260
|
reflowIntoBounds();
|
|
12469
13261
|
// Armed AFTER the mount settle: the first reading is only the baseline the next
|
|
@@ -12503,6 +13295,14 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
12503
13295
|
}
|
|
12504
13296
|
pollTimer = null;
|
|
12505
13297
|
}
|
|
13298
|
+
if (themeWatcher !== null) {
|
|
13299
|
+
try {
|
|
13300
|
+
themeWatcher.disconnect();
|
|
13301
|
+
} catch (error) {
|
|
13302
|
+
// Already gone with the page.
|
|
13303
|
+
}
|
|
13304
|
+
themeWatcher = null;
|
|
13305
|
+
}
|
|
12506
13306
|
pollBounds = null;
|
|
12507
13307
|
armed = false;
|
|
12508
13308
|
leaving = false;
|
|
@@ -12853,10 +13653,13 @@ function apply(ctx) {
|
|
|
12853
13653
|
Date.now(),
|
|
12854
13654
|
PAINT_PATIENCE_MS
|
|
12855
13655
|
);
|
|
12856
|
-
//
|
|
12857
|
-
//
|
|
13656
|
+
// Per-theme is honoured exactly as stored (0.7.0). This used to be forced to
|
|
13657
|
+
// `false` here because nothing in the UI could edit the dark set, so a stored
|
|
13658
|
+
// flag could only produce rules the reader had no way to understand. The
|
|
13659
|
+
// card's theme control is that way now: `normalizeConfig` has already coerced
|
|
13660
|
+
// the flag and decoded the map, and `buildFontCss` emits the second,
|
|
13661
|
+
// theme-prefixed ruleset whenever the two sets actually differ.
|
|
12858
13662
|
var unified = normalizeConfig(shared.overlayPendingValues(snapshot.value, pendingLocalValues));
|
|
12859
|
-
unified[PER_THEME_FIELD] = false;
|
|
12860
13663
|
probeLog("sync", {
|
|
12861
13664
|
overlay: Object.keys(pendingLocalValues).map(function (key) {
|
|
12862
13665
|
return key + "=" + String(pendingLocalValues[key].value);
|